2026 04 05 Phase20 Certificate Pdf Quiz Design
Phase 20: Certificate PDF Generation & Quiz Enhancements — Design Spec
Date: 2026-04-05 Status: Approved Phase: 20 of 39 Milestone: v2.0
Overview#
Generate downloadable PDF certificates on course completion and add explanatory feedback to quiz results. Two epics, sequential: Epic 1 (Certificate PDF) then Epic 2 (Quiz Enhancements).
Epic 1: Certificate PDF Generation#
Goal#
Auto-generate branded PDF certificates when a learner completes a course. Certificates are downloadable and publicly verifiable via QR code.
Technology Choices#
Certificate Layout#
Fixed single-page A4 landscape PDF:
- Top: Platform logo (embedded PNG)
- Center: "Certificate of Completion" heading
- Body: Learner full name (large), course title, platform + level badge text, completion date (formatted)
- Bottom-left: Unique certificate ID
- Bottom-right: QR code linking to
{BASE_URL}/verify/{completion_id} - Typography: Helvetica (built-in to fpdf, no font embedding needed)
Data Flow#
1All lessons marked complete2 → checkAndCompleteEnrollment() triggers3 → CompleteEnrollment(enrollmentID)4 → Create Completion record (UUID)5 → GenerateCertificatePDF(completion, course, userFullName)6 → Render PDF via fpdf7 → Embed QR code via go-qrcode8 → Write to data/certificates/{completion_id}.pdf9 → Update Completion.CertURL = /api/v1/certificates/{id}/download10 → Return CompletionNew Files#
API Endpoints#
Existing (unchanged):
GET /api/v1/certificates/:id— returns completion metadata JSON
New:
-
GET /api/v1/certificates/:id/download— streams PDF file- Response:
Content-Type: application/pdf,Content-Disposition: attachment; filename="certificate-{id}.pdf" - 404 if completion not found or PDF file missing
- Public (no auth) — certificate ID is unguessable UUID
- Response:
-
GET /api/v1/certificates/:id/verify— public verification JSON- Response:
{ valid: true, course_title, learner_name, platform, level, completed_at } - 404 if not found →
{ valid: false } - Public (no auth) — this is the QR code landing endpoint
- Response:
Frontend Changes#
apps/learn/app/(protected)/certificates/page.tsx:
- Change download link from current completion endpoint to
/api/v1/certificates/{completion_id}/download - Add "Download PDF" button with download icon
apps/learn/app/(course)/verify/[id]/page.tsx (new):
- Public page (no auth required)
- Fetches
/api/v1/certificates/{id}/verify - Displays: certificate validity badge, course title, learner name, completion date
- Shows "Invalid Certificate" if not found
Storage#
- Directory:
data/certificates/relative to backend working directory - File naming:
{completion_id}.pdf(UUID, globally unique) - Created on first certificate generation (mkdir -p equivalent)
- No cleanup/expiry needed — certificates are permanent
- Future migration: swap file I/O in
certificate.gofor S3 client; endpoint stays the same
Error Handling#
- PDF generation failure: log error, still create Completion record but leave CertURL empty. Certificate can be regenerated later via admin endpoint (future).
- File not found on download: return 404 with JSON error
- Storage directory not writable: fail loudly at startup (check in main.go init)
Epic 2: Quiz Hints & Explanations#
Goal#
Show per-question explanations and correct answers after quiz submission. Add retry cooldown to prevent spam.
Domain Model Changes#
backend/internal/learn/domain/quiz.go:
1type Question struct {2 // ... existing fields ...3 Explanation string `json:"explanation" gorm:"type:text;default:''"`4}56type Option struct {7 // ... existing fields ...8 Hint string `json:"hint" gorm:"type:text;default:''"`9}1011type Quiz struct {12 // ... existing fields ...13 CooldownMinutes int `json:"cooldown_minutes" gorm:"default:0"`14}Database Migration#
New migration 003_add_quiz_explanations.up.sql:
1ALTER TABLE questions ADD COLUMN IF NOT EXISTS explanation TEXT DEFAULT '';2ALTER TABLE options ADD COLUMN IF NOT EXISTS hint TEXT DEFAULT '';3ALTER TABLE quizzes ADD COLUMN IF NOT EXISTS cooldown_minutes INTEGER DEFAULT 0;Down migration:
1ALTER TABLE questions DROP COLUMN IF EXISTS explanation;2ALTER TABLE options DROP COLUMN IF EXISTS hint;3ALTER TABLE quizzes DROP COLUMN IF EXISTS cooldown_minutes;Quiz Submission Response Change#
Current response:
1{ "score": 85, "passed": true, "correct": 4, "total": 5 }New response (after submission only):
1{2 "score": 85,3 "passed": true,4 "correct": 4,5 "total": 5,6 "questions": [7 {8 "id": "uuid",9 "text": "What is Kubernetes?",10 "explanation": "Kubernetes is a container orchestration platform...",11 "your_answer": "option-uuid-1",12 "correct_answer": "option-uuid-2",13 "is_correct": false,14 "options": [15 { "id": "option-uuid-1", "text": "A database", "hint": "Databases store data, not orchestrate containers" },16 { "id": "option-uuid-2", "text": "A container orchestrator", "hint": "", "is_correct": true }17 ]18 }19 ]20}Important: GET /api/v1/quizzes/:id continues to strip is_correct, explanation, and hint from responses. These are only revealed in the submission response.
Retry Cooldown#
Quiz.CooldownMinutes(default 0 = no cooldown)- On
POST /api/v1/quizzes/:id/submit:- Check last attempt timestamp for this user+quiz
- If
now - last_attempt < cooldown_minutes→ return 429 withRetry-Afterheader (seconds remaining)
- Frontend shows countdown timer when blocked
Frontend Changes#
apps/learn/components/quiz/quiz-container.tsx:
After submission, render result view with:
- Per-question list showing:
- Question text
- User's selected answer (highlighted red if wrong, green if correct)
- Correct answer (highlighted green)
- Explanation text below each question
- Option hints (shown on incorrect options if available)
- Overall score and pass/fail status (existing)
- If cooldown active: show "You can retry in X minutes" with countdown
Error Handling#
- Cooldown check: 429 status with
retry_after_secondsin JSON body - Missing explanations: gracefully handle empty strings (don't show explanation section if empty)
Verification Matrix#
Dependencies#
github.com/go-pdf/fpdf— PDF generationgithub.com/skip2/go-qrcode— QR code generation- No new frontend dependencies
Files Changed (Summary)#
New#
backend/internal/learn/service/certificate.gobackend/migrations/003_add_quiz_explanations.up.sqlbackend/migrations/003_add_quiz_explanations.down.sqlapps/learn/app/(course)/verify/[id]/page.tsx
Modified#
backend/go.mod+backend/go.sum(new deps)backend/internal/learn/domain/quiz.go(Explanation, Hint, CooldownMinutes fields)backend/internal/learn/service/learn_service.go(call PDF generation in CompleteEnrollment)backend/internal/learn/handler/certificate_handler.go(download + verify endpoints)backend/internal/learn/handler/quiz_handler.go(explanations in submit response, cooldown check)backend/cmd/learn-api/main.go(register new routes, storage dir check)apps/learn/app/(protected)/certificates/page.tsx(download button URL)apps/learn/components/quiz/quiz-container.tsx(result view with explanations)