# Epic 2 Retrospektif — Ujian & Sertifikat (LMS-BGN) Tanggal: 2025-11-12 Cakupan: Siklus sesi ujian, penilaian, ringkasan, daftar ujian, pengiriman ulang sertifikat ## Ringkasan Hasil - Endpoint backend untuk Epic 2 telah dibuat dan diverifikasi dengan pengujian. - Contoh REST Client dan cURL tersedia untuk QA manual. - Kontrak mengikuti `docs/api/openapi.yaml` dan diselaraskan dengan kebutuhan UI inti. - Integrasi frontend sengaja ditunda sesuai instruksi; fokus pada pelurusan dokumentasi. ## Cakupan yang Diimplementasikan - `POST /api/exam-session` — membuat sesi ujian (validasi dasar). - `POST /api/exam-session/{sessionId}/score` — penilaian batch dengan `idempotencyKey`. - `GET /api/exam-session/{sessionId}/summary` — ringkasan hasil teragregasi. - `GET /api/exams` — daftar ujian terpaginasikan dengan filter teks `q`. - `POST /api/certificates/{certificateId}/resend` — pengiriman ulang email (diterima + throttling). ## Cakupan OpenAPI & Kuis Interaktif - `ExamSessionCreateRequest`: saat ini memuat `examId` dan `userId`. Mode interaktif ditentukan dari metadata ujian di sisi server (lihat `ExamListItem.isInteractive`). Tidak ada skema bernama `InteractiveExam` terpisah pada spesifikasi sekarang. - `ExamScoreRequest`: memuat `mode` (`batch|stream`), `idempotencyKey`, dan `answers[]` dengan bentuk sederhana `{ questionId, choiceIndex }`. Bentuk ini belum mencakup tipe interaktif (hotspot, puzzle, scenario) dari `sampleInteractiveExamData`; perlu perluasan skema di fase selanjutnya. - `ExamSummaryResponse`: bentuk respons diselaraskan dengan kebutuhan halaman ringkasan (judul, skor, total, jawaban per soal) dan sesuai contoh di spesifikasi. - `ExamListItem`: memuat `isInteractive: boolean` sehingga UI dapat membedakan kuis interaktif di listing. - Konsekuensi: Integrasi penuh fitur interaktif memerlukan penambahan skema dan payload yang lebih kaya (mis. struktur hotspot, puzzle, scenario) ke OpenAPI sebelum wiring frontend. ## Kualitas & Metrik - Pengujian: 17 lulus, 0 gagal (Jest + Supertest). - Durasi suite: ~1.8s lokal. - Artefak QA: - REST Client: `docs/qa/assessments/backend-endpoints.http` - cURL: `docs/qa/assessments/curl-examples.md` ## Hal yang Berjalan Baik - Bootstrap backend cepat dengan Express; pemisahan concern jelas. - Idempotensi & throttling dasar untuk route sensitif. - Pengujian menyentuh happy-path dan kasus error inti. - Dokumentasi dan contoh memudahkan verifikasi manual. ## Tantangan - Integrasi frontend belum dihubungkan: `src/app/exam-session/page.tsx`, `exam-summary`, dan `exams` masih butuh wiring API (ditunda sesuai instruksi). - Data masih in-memory; belum ada persistence atau auth sehingga alur belum realistis. - Kesenjangan pemetaan `sampleInteractiveExamData` ke payload OpenAPI saat ini (belum ada skema interaktif rinci). ## Risiko & Mitigasi - Drift kontrak vs ekspektasi frontend — Mitigasi: patuhi `docs/api/openapi.yaml`, perluas skema untuk interaktif, dan sesuaikan pengujian. - Deliverability email & audit — Mitigasi: rencanakan konfigurasi SMTP, audit log, dan retry/backoff. - Tidak ada persistence — Mitigasi: definisikan skema minimal, tambah layer DB ringan, pindahkan state idempotensi dari memori ke storage. - Keamanan & rate limiting — Mitigasi: tambahkan auth checks dan rate-limit untuk route ujian & sertifikat. ## Tindak Lanjut (Tanpa Implementasi Langsung) - Perluas OpenAPI untuk mendukung tipe interaktif (hotspot, puzzle, scenario) dan bentuk jawaban yang relevan. - Definisikan pemetaan dari `sampleInteractiveExamData` ke payload backend (create/score/summary) dan tambahkan contoh di dokumentasi. - Tambah persistence minimal untuk sesi & jawaban; pertimbangkan auth dasar. - Siapkan rencana wiring frontend ke backend untuk sprint berikutnya, sesuai skema yang diperluas. - CI: jalankan `npm test` pada push/PR. - Observabilitas: logging terstruktur & audit trail untuk alur ujian/sertifikat. ## Referensi - Konteks: `docs/epic-2-context.md` - OpenAPI: `docs/api/openapi.yaml` - QA: `docs/qa/assessments/backend-endpoints.http`, `docs/qa/assessments/curl-examples.md` - Frontend (belum terhubung): `src/app/exam-session/page.tsx`, `src/app/exam-summary/page.tsx`, `src/app/exams/page.tsx` ## Contoh Payload — Interaktif (Draft) Catatan: Contoh berikut bertujuan memudahkan diskusi skema. Beberapa field bertanda "proposal" tidak ada dalam spesifikasi saat ini dan akan diajukan di perluasan OpenAPI berikutnya. ### Create (proposal ekstensi) Request: ```json { "examId": "interactive-dapur-mbg-001", "userId": "user-123", "isInteractive": true, "idempotencyKey": "create-5a1d9e2c-9f12-4d3a-b3a1-7c8e0b4f6a11", "clientContext": { "device": "web", "appVersion": "0.1.0", "locale": "id-ID" } } ``` Response (sesuai spesifikasi saat ini): ```json { "sessionId": "sess-001", "status": "created", "startTime": "2025-11-12T08:00:00Z" } ``` ### Score (proposal dukungan tipe interaktif) Request: ```json { "mode": "batch", "idempotencyKey": "score-8d3bc2f0-26a3-4fd0-9b4c-5e2b7b1f9c77", "answers": [ { "questionId": "1", "type": "enhanced_mcq", "choiceIndex": 0, "confidence": 4 }, { "questionId": "video-scenario-1", "type": "video_scenario", "scenarioAnswers": [ { "stepId": "step1", "selectedOptionId": "opt4" }, { "stepId": "step2", "selectedOptionId": "opt4" } ]}, { "questionId": "hotspot-scenario-1", "type": "image_hotspot", "selectedHotspots": ["hotspot1", "hotspot2", "hotspot4"] }, { "questionId": "media-gallery-1", "type": "media_gallery", "viewedItems": ["video1", "image1", "video2", "image2", "document1", "image3"] }, { "questionId": "3", "type": "puzzle", "matches": [ { "pieceId": "ccp", "targetId": "def1" }, { "pieceId": "haccp", "targetId": "def2" }, { "pieceId": "sanitasi", "targetId": "def3" }, { "pieceId": "kontaminasi", "targetId": "def4" } ]}, { "questionId": "6", "type": "scenario", "selectedOptionId": "option2" } ] } ``` Response (contoh ringkas): ```json { "totalScore": 85, "totalQuestions": 10, "correctAnswers": 8, "scorePercent": 85, "perQuestion": [ { "questionId": "1", "isCorrect": true, "earnedPoints": 10, "maxPoints": 10 }, { "questionId": "video-scenario-1", "isCorrect": true, "earnedPoints": 20, "maxPoints": 20 } ] } ``` ### Summary (kompatibel + proposal detail) Response (kompatibel): ```json { "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" }, { "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" }, { "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" }, { "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" } ] } ``` Catatan kompatibilitas: - Agar tetap kompatibel, `userAnswer` untuk tipe interaktif diringkas sebagai string; perluasan dapat menambahkan `details` opsional berisi struktur lengkap per tipe (proposal). ## Catatan Implementasi (Update) - Artefak QA telah disinkronkan: payload union untuk `ExamScoreRequest` tersedia di `docs/qa/assessments/backend-endpoints.http`, termasuk blok uji MCQ-only untuk hasil skor positif. - Backend kini memparse dan menilai tipe interaktif (video_scenario, image_hotspot, media_gallery, puzzle, scenario) dengan skema sederhana, termasuk partial scoring per tipe; bentuk `{ questionId, choiceIndex }` tetap didukung untuk MCQ. - Respons `GET /api/exam-session/{sessionId}/summary` sekarang menyertakan `answers[].details` per tipe (langkah video, hotspot terpilih vs kunci, progres gallery, langkah puzzle, dan opsi scenario) sehingga kompatibel + kaya detail sesuai contoh draft. - Pemeriksaan Authorization (Bearer) diterapkan server-side untuk endpoint ujian: create session, score, dan summary. QA contoh sudah menyertakan header `Authorization: Bearer test-token`. ### NFR & Observabilitas (Ringkas) - Logging terstruktur dengan korelasi `requestId`/`sessionId` untuk create/score/summary/resend - Metrics: `score_latency_ms`, `summary_latency_ms`, `error_rate`, `resend_throttle_hits` - Alerting sederhana untuk lonjakan error rata-rata p95 - Audit trail untuk `resend` (recipient, status, attempt) ### Dependencies (Versi Target) - Node.js `18.x`, Express `4.x`, Jest `29.x`, Supertest `6.x`, Nodemailer `6.x` ### Acceptance Criteria Atomik + Pemetaan Test - POST `/api/exam-session` membuat sesi valid → tes integrasi create (Bearer wajib) - POST `/api/exam-session/{sessionId}/score` menghitung skor (MCQ+interaktif) & idempotent → tes integrasi score + idempotency - GET `/api/exam-session/{sessionId}/summary` menampilkan agregat & `answers[].details` → tes integrasi summary - POST `/api/certificates/{certificateId}/resend` throttling + audit → tes integrasi resend ### Traceability Matrix (Ringkas) | AC ID | Bagian | Komponen | Story | Tes | |------|--------|----------|-------|-----| | R-AC1 | Create Session | `server.js` route create | `docs/stories/2-1-exam-session-create-api.md` | `backend/tests/server.test.js` (create session) | | R-AC2 | Scoring & Idempotency | scorer util + route score | `docs/stories/2-2-exam-scoring-api.md` | `backend/tests/server.test.js` (score + idempotency) | | R-AC3 | Summary kompatibel | summary builder + route summary | `docs/stories/2-3-exam-summary-read-api.md` | `backend/tests/server.test.js` (summary) | | R-AC4 | Resend throttled + audit | resend handler | TBD: `docs/stories/2-4-certificate-resend-api.md` | `backend/tests/server.test.js` (resend) |