openapi: 3.0.3 info: title: LMS-BGN API — Draft Skeleton version: 0.1.0-draft license: name: MIT url: https://opensource.org/licenses/MIT description: Non-implementasi skeleton untuk stories Epik 1 & 2. x-status: drafted x-contract-principles: align_to_frontend_models: true sources: - path: src/app/course/[courseId]/page.tsx note: Course & Module fields - path: src/app/exam-session/page.tsx note: Exam Session UI shape - path: src/app/exam-summary/page.tsx note: Exam Summary UI shape - path: src/app/exams/page.tsx note: Exams listing UI servers: - url: https://api.lms-bgn.dev description: Draft dev server tags: - name: Courses description: Operasi daftar dan detail kursus - name: Modules description: Progres dan ringkasan modul - name: Assignments description: Pengumpulan tugas dan respons - name: Exams description: Sesi ujian, penskoran, dan ringkasan - name: Certificates description: Sertifikat dan pengiriman ulang email security: - bearerAuth: [] paths: /api/courses: get: tags: [Courses] summary: List Courses operationId: listCourses x-story-id: 1-1-course-catalog-read-api parameters: - name: page in: query schema: type: integer minimum: 1 default: 1 - name: pageSize in: query schema: type: integer minimum: 1 maximum: 100 default: 20 - name: q in: query schema: type: string - name: category in: query schema: type: string - name: level in: query schema: type: string - name: sortBy in: query schema: type: string enum: [updatedAt, title] - name: order in: query schema: type: string enum: [asc, desc] responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/CourseListResponse' example: page: 1 pageSize: 20 total: 2 items: - id: "c1" title: "Basic Knife Skills" instructor: "Chef A" progress: 30 status: "in_progress" color: "#FF9800" thumbnail: "/images/knife-skills.jpg" duration: "6h" modules: 12 enrolled: "1.2k" - id: "c2" title: "Bread Baking 101" instructor: "Chef B" progress: 0 status: "not_started" color: "#3F51B5" thumbnail: "/images/bread.jpg" duration: "4h" modules: 8 enrolled: "860" "400": description: Invalid query parameters /api/courses/{courseId}: get: tags: [Courses] summary: Get Course Detail operationId: getCourseDetail x-story-id: 1-2-course-detail-read-api parameters: - name: courseId in: path required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/CourseDetailResponse' example: id: "c1" title: "Basic Knife Skills" description: "Learn core knife handling techniques." instructor: "Chef A" duration: "6h" totalModules: 12 completedModules: 3 progress: 25 thumbnail: "/images/knife-skills.jpg" color: "#FF9800" modules: - id: "m1" title: "Intro" description: "Overview" duration: "15m" isUnlocked: true isCompleted: false progress: 0 type: "reading" hasQuiz: false hasAssignment: false "404": description: Not Found /api/modules/{moduleId}/progress: post: tags: [Modules] summary: Update Module Progress operationId: updateModuleProgress x-story-id: 1-3-module-progress-update-api parameters: - name: moduleId in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ModuleProgressUpdateRequest' responses: "202": description: Accepted content: application/json: schema: $ref: '#/components/schemas/ModuleProgressUpdateResponse' "400": description: Invalid request /api/assignments/{assignmentId}/submission: post: tags: [Assignments] summary: Persist Assignment Submission operationId: persistAssignmentSubmission x-story-id: 1-4-assignment-submission-persist parameters: - name: assignmentId in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AssignmentSubmissionRequest' responses: "201": description: Created content: application/json: schema: $ref: '#/components/schemas/AssignmentSubmissionResponse' "400": description: Invalid request /api/exam-session: post: tags: [Exams] summary: Create Exam Session operationId: createExamSession x-story-id: 2-1-exam-session-create-api requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ExamSessionCreateRequest' responses: "201": description: Created content: application/json: schema: $ref: '#/components/schemas/ExamSessionCreateResponse' example: sessionId: "sess-001" status: "created" startTime: "2024-01-26T09:00:00Z" "400": description: Invalid request "409": description: Conflict (duplicate idempotency key) "429": description: Too Many Requests /api/exam-session/{sessionId}/score: post: tags: [Exams] summary: Score Exam Session operationId: scoreExamSession x-story-id: 2-2-exam-scoring-api parameters: - name: sessionId in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ExamScoreRequest' responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ExamScoreResponse' example: totalScore: 85 totalQuestions: 10 correctAnswers: 8 scorePercent: 85 perQuestion: - questionId: "q1" isCorrect: true earnedPoints: 10 maxPoints: 10 - questionId: "q2" isCorrect: false earnedPoints: 0 maxPoints: 10 "400": description: Invalid request "404": description: Session not found "409": description: Conflict (duplicate idempotency key) "429": description: Too Many Requests /api/exam-session/{sessionId}/summary: get: tags: [Exams] summary: Read Exam Summary operationId: readExamSummary x-story-id: 2-3-exam-summary-read-api parameters: - name: sessionId in: path required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ExamSummaryResponse' example: examId: "exam-001" examTitle: "Kuis Interaktif - Keamanan Dapur BGN" totalQuestions: 10 correctAnswers: 8 score: 85 timeSpent: "25 menit 30 detik" completedAt: "2024-01-26T09:30:00Z" answers: - questionId: "q1" question: "Apa langkah pertama memastikan kebersihan dapur?" userAnswer: "Mencuci tangan sebelum mulai" correctAnswer: "Mencuci tangan sebelum mulai" isCorrect: true type: "enhanced_mcq" - questionId: "q2" question: "Pada suhu berapa ayam harus dimasak?" userAnswer: "60°C" correctAnswer: "75°C" isCorrect: false type: "enhanced_mcq" "400": description: Invalid request /api/certificates/{certificateId}/resend: post: tags: [Certificates] summary: Resend Certificate Email (Stub) operationId: resendCertificateEmail x-story-id: 2-4-certificate-resend-email-stub parameters: - name: certificateId in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CertificateResendRequest' responses: "202": description: Accepted content: application/json: schema: $ref: '#/components/schemas/CertificateResendResponse' "400": description: Invalid request /api/exams: get: tags: [Exams] summary: List Exams operationId: listExams parameters: - name: page in: query schema: type: integer minimum: 1 default: 1 - name: pageSize in: query schema: type: integer minimum: 1 maximum: 100 default: 20 - name: q in: query schema: type: string - name: status in: query schema: type: string enum: [upcoming, available, completed, missed] - name: type in: query schema: type: string - name: dateFrom in: query schema: type: string format: date - name: dateTo in: query schema: type: string format: date responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ExamListResponse' example: page: 1 pageSize: 20 total: 5 items: - id: 1 title: "Kuis Interaktif - Keamanan Dapur BGN" course: "Keamanan Pangan dan Gizi - Badan Gizi Nasional" type: "Interactive Quiz" date: "2024-01-26" startTime: "09:00" endTime: "10:00" duration: 60 questions: 10 status: "available" attempts: 0 maxAttempts: 3 passingScore: 70 description: "Kuis interaktif tentang keamanan pangan dan praktik dapur yang baik di lingkungan BGN" instructions: ["Kuis ini menggunakan berbagai jenis soal interaktif", "Termasuk drag & drop, simulasi dapur, dan skenario nyata", "Baca instruksi setiap soal dengan teliti"] isInteractive: true - id: 2 title: "Ujian Tengah Semester - HACCP" course: "HACCP - Hazard Analysis Critical Control Point" type: "Midterm Exam" date: "2024-01-28" startTime: "10:00" endTime: "12:00" duration: 120 questions: 50 status: "upcoming" attempts: 0 maxAttempts: 1 passingScore: 75 description: "Ujian tengah semester mencakup materi HACCP dari modul 1-6" instructions: ["Pastikan koneksi internet stabil", "Siapkan alat tulis", "Baca soal dengan teliti"] "400": description: Invalid query parameters components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT schemas: CourseSummary: type: object properties: id: { type: string } title: { type: string } instructor: { type: string } progress: { type: integer } status: { type: string } color: { type: string } thumbnail: { type: string } duration: { type: string } modules: { type: integer } enrolled: { type: string } CourseListResponse: type: object properties: page: { type: integer } pageSize: { type: integer } total: { type: integer } items: type: array items: $ref: '#/components/schemas/CourseSummary' ModuleSummary: type: object properties: id: { type: string } title: { type: string } description: { type: string } duration: { type: string } isUnlocked: { type: boolean } isCompleted: { type: boolean } progress: { type: integer } type: { type: string, enum: [video, reading, quiz, assignment] } hasQuiz: { type: boolean } hasAssignment: { type: boolean } CourseDetailResponse: type: object properties: id: { type: string } title: { type: string } description: { type: string } instructor: { type: string } duration: { type: string } totalModules: { type: integer } completedModules: { type: integer } progress: { type: integer } thumbnail: { type: string } color: { type: string } modules: type: array items: $ref: '#/components/schemas/ModuleSummary' ModuleProgressUpdateRequest: type: object properties: progressPercent: { type: integer, minimum: 0, maximum: 100 } idempotencyKey: { type: string } updatedAt: { type: string, format: date-time } ModuleProgressUpdateResponse: type: object properties: status: { type: string, enum: [accepted] } AssignmentSubmissionRequest: type: object properties: content: { type: string } attachments: type: array items: type: string idempotencyKey: { type: string } AssignmentSubmissionResponse: type: object properties: submissionId: { type: string } status: { type: string, enum: [created] } ExamSessionCreateRequest: type: object properties: examId: { type: string } userId: { type: string } isInteractive: { type: boolean } idempotencyKey: { type: string } clientContext: type: object properties: device: { type: string } appVersion: { type: string } locale: { type: string } ExamSessionCreateResponse: type: object properties: sessionId: { type: string } status: { type: string, enum: [created] } startTime: { type: string, format: date-time } ExamScoreRequest: type: object properties: mode: { type: string, enum: [batch, stream] } idempotencyKey: { type: string } answers: type: array items: oneOf: - $ref: '#/components/schemas/EnhancedMcqAnswer' - $ref: '#/components/schemas/VideoScenarioAnswer' - $ref: '#/components/schemas/ImageHotspotAnswer' - $ref: '#/components/schemas/MediaGalleryAnswer' - $ref: '#/components/schemas/PuzzleAnswer' - $ref: '#/components/schemas/ScenarioAnswer' example: mode: batch idempotencyKey: score-sess-001 answers: - questionId: mcq-1 type: enhanced_mcq choiceIndex: 2 confidence: 4 - questionId: video-1 type: video_scenario scenarioAnswers: - stepId: s1 selectedOptionId: optA - stepId: s2 selectedOptionId: optB - questionId: hotspot-1 type: image_hotspot selectedHotspots: [spot1, spot3] - questionId: gallery-1 type: media_gallery viewedItems: [itemA, itemB, itemC] - questionId: puzzle-1 type: puzzle matches: - pieceId: p1 targetId: t1 - pieceId: p2 targetId: t2 - questionId: scenario-1 type: scenario selectedOptionId: optX ExamScoreResponse: type: object properties: totalScore: { type: number } totalQuestions: { type: integer } correctAnswers: { type: integer } scorePercent: { type: number } perQuestion: type: array items: type: object properties: questionId: { type: string } isCorrect: { type: boolean } earnedPoints: { type: number } maxPoints: { type: number } # Interactive Answer union items EnhancedMcqAnswer: type: object required: [questionId, type, choiceIndex] properties: questionId: { type: string } type: { type: string, enum: [enhanced_mcq] } choiceIndex: { type: integer } confidence: { type: integer, minimum: 0, maximum: 5 } VideoScenarioAnswer: type: object required: [questionId, type, scenarioAnswers] properties: questionId: { type: string } type: { type: string, enum: [video_scenario] } scenarioAnswers: type: array items: type: object required: [stepId, selectedOptionId] properties: stepId: { type: string } selectedOptionId: { type: string } ImageHotspotAnswer: type: object required: [questionId, type, selectedHotspots] properties: questionId: { type: string } type: { type: string, enum: [image_hotspot] } selectedHotspots: type: array items: { type: string } MediaGalleryAnswer: type: object required: [questionId, type, viewedItems] properties: questionId: { type: string } type: { type: string, enum: [media_gallery] } viewedItems: type: array items: { type: string } PuzzleAnswer: type: object required: [questionId, type, matches] properties: questionId: { type: string } type: { type: string, enum: [puzzle] } matches: type: array items: type: object required: [pieceId, targetId] properties: pieceId: { type: string } targetId: { type: string } ScenarioAnswer: type: object required: [questionId, type, selectedOptionId] properties: questionId: { type: string } type: { type: string, enum: [scenario] } selectedOptionId: { type: string } # Interactive Summary details union items EnhancedMcqDetails: type: object properties: choiceIndex: { type: integer } confidence: { type: integer, minimum: 0, maximum: 5 } VideoScenarioDetails: type: object properties: steps: type: array items: type: object properties: stepId: { type: string } selectedOptionId: { type: string } isCorrect: { type: boolean } earnedPoints: { type: number } ImageHotspotDetails: type: object properties: selectedHotspots: type: array items: { type: string } correctHotspots: type: array items: { type: string } MediaGalleryDetails: type: object properties: viewedItems: type: array items: { type: string } requiredItems: type: array items: { type: string } completionRate: { type: number } PuzzleDetails: type: object properties: matches: type: array items: type: object properties: pieceId: { type: string } targetId: { type: string } correctPairs: { type: integer } earnedPoints: { type: number } ScenarioDetails: type: object properties: selectedOptionId: { type: string } isCorrect: { type: boolean } ExamSummaryResponse: type: object properties: examId: { type: string } examTitle: { type: string } totalQuestions: { type: integer } correctAnswers: { type: integer } score: { type: number } timeSpent: { type: string } completedAt: { type: string, format: date-time } answers: type: array items: type: object properties: questionId: { type: string } question: { type: string } userAnswer: { type: string } correctAnswer: { type: string } isCorrect: { type: boolean } type: { type: string, enum: [enhanced_mcq, video_scenario, image_hotspot, media_gallery, puzzle, scenario] } details: oneOf: - $ref: '#/components/schemas/EnhancedMcqDetails' - $ref: '#/components/schemas/VideoScenarioDetails' - $ref: '#/components/schemas/ImageHotspotDetails' - $ref: '#/components/schemas/MediaGalleryDetails' - $ref: '#/components/schemas/PuzzleDetails' - $ref: '#/components/schemas/ScenarioDetails' example: examId: interactive-dapur-mbg-001 examTitle: Kuis Interaktif Dapur MBG totalQuestions: 10 correctAnswers: 8 score: 85 timeSpent: "25 menit 30 detik" completedAt: "2025-11-12T08:30:00Z" answers: - questionId: "1" question: "Apa langkah pertama dalam memastikan keamanan pangan di Dapur MBG?" userAnswer: "Melakukan analisis bahaya" correctAnswer: "Melakukan analisis bahaya" isCorrect: true type: enhanced_mcq details: choiceIndex: 0 confidence: 4 - questionId: "video-scenario-1" question: "Analisis Situasi Dapur: Identifikasi Masalah Keamanan Pangan" userAnswer: "step1: opt4; step2: opt4" correctAnswer: "step1: opt4; step2: opt4" isCorrect: true type: video_scenario details: steps: - stepId: step1 selectedOptionId: opt4 isCorrect: true earnedPoints: 10 - stepId: step2 selectedOptionId: opt4 isCorrect: true earnedPoints: 10 - questionId: "hotspot-scenario-1" question: "Identifikasi Area Bermasalah dalam Tata Letak Dapur" userAnswer: "hotspot1,hotspot2,hotspot4" correctAnswer: "hotspot1,hotspot2,hotspot3,hotspot4,hotspot5" isCorrect: false type: image_hotspot details: selectedHotspots: [hotspot1, hotspot2, hotspot4] correctHotspots: [hotspot1, hotspot2, hotspot3, hotspot4, hotspot5] - questionId: "3" question: "Cocokkan istilah keamanan pangan dengan definisinya" userAnswer: "ccp->def1; haccp->def2; sanitasi->def3; kontaminasi->def4" correctAnswer: "ccp->def1; haccp->def2; sanitasi->def3; kontaminasi->def4" isCorrect: true type: puzzle details: matches: - pieceId: ccp targetId: def1 - pieceId: haccp targetId: def2 correctPairs: 4 earnedPoints: 10 CertificateResendRequest: type: object properties: recipientEmail: { type: string, format: email } idempotencyKey: { type: string } CertificateResendResponse: type: object properties: jobId: { type: string } status: { type: string, enum: [accepted, throttled] } ExamListItem: type: object properties: id: { type: integer } title: { type: string } course: { type: string } type: { type: string } date: { type: string, format: date } startTime: { type: string } endTime: { type: string } duration: { type: integer } questions: { type: integer } status: { type: string, enum: [upcoming, available, completed, missed] } attempts: { type: integer } maxAttempts: { type: integer } passingScore: { type: integer } description: { type: string } instructions: type: array items: { type: string } isInteractive: { type: boolean } score: { type: integer } grade: { type: string } ExamListResponse: type: object properties: page: { type: integer } pageSize: { type: integer } total: { type: integer } items: type: array items: $ref: '#/components/schemas/ExamListItem'