Training / chapter / lesson completion + quiz/exam progress + score leaderboard
This file documentsUserProgressAnalyticsController — a read-only analytics layer that derives a student’s learning progress and a gamification score from data that already exists in the system.
It does NOT modify the existing quiz/exam progress-tracking endpoints. Those (start_student_quiz,finish_student_quiz,save_student_quiz_result,show_quiz_student_answers,reset_student_attempts) remain the single writer of attempts/answers and are documented in Quizzes & Exams progress tracking. This controller only reads their tables.
Table of Contents
- Overview
- Base URL and Headers
- How progress is computed (sources of truth)
- How the gamification score is computed
- Authorization & company scoping
- Endpoints
- 6.1 Training Progress (detailed, one training)
- 6.2 Trainings Progress (summary list)
- 6.3 Quiz/Exam Progress (per-question breakdown)
- 6.4 Quizzes/Exams Progress (summary list)
- 6.5 Gamification Score (recompute + rank)
- 6.6 Leaderboard (top-N)
- 6.7 Recompute Scores (bulk, staff)
- Per-question correctness model
- Configuration
- History Tracking Actions
- Related Files
- Future Suggestions
1. Overview
The system already records two facts elsewhere:- Lesson completion — a row in the
student_lessonpivot (written byLessonsController@toggle_lesson_validation). - Quiz/exam attempts & scores — rows in
quiz_student_attempts/quiz_student_answers(written by the progress-tracking controller).
trainings → chapters → lessons and quizzes), to produce:
- Per-training progress — which lessons are completed per chapter, how many quizzes are passed, and overall progress %.
- Per-quiz/exam progress — best score, pass state, and a per-question right/wrong breakdown for any attempt.
- A gamification score per user (
users.gamification_score) and a top-N leaderboard for the company.
2. Base URL and Headers
progress prefix and the tenant-scoped middleware stack (auth:sanctum, check_if_user_is_blocked_middleware, company_policy_middleware).
3. How progress is computed (sources of truth)
⚠️Training progress percentages (each lesson and each quiz counts as one “item”):passed_atis NOT used as the pass signal. In the existing tracking controllerpassed_atis set unconditionally whenever a result is saved, so it does not by itself mean “passed”. The reliable signal isscore_percentage >= passing_percentage, which is what this controller uses everywhere.
0.0 (never divide-by-zero).
4. How the gamification score is computed
- Idempotent / no double-counting — each completed lesson and each distinct passed quiz/exam is counted exactly once, no matter how many attempts or re-toggles happened.
- Stored denormalized in
users.gamification_score(anunsignedInteger, indexed) purely for fast leaderboard reads. It is rebuilt from the sources of truth byGamificationService, never hand-edited. - It is separate from
loyalty_points(which is monetary / convertible to coupons). Gamification score is a learning/achievement metric only.
- On demand for one user via
GET /api/progress/score(recomputes that user before returning). - In bulk for a whole company via
POST /api/progress/recompute-scores(run before showing a fresh leaderboard).
5. Authorization & company scoping
- Company scoping is manual. Every endpoint requires
company_id;CompanyPolicyMiddlewareguarantees it matches the caller’s company (super-admin bypasses). - Who can view a student’s progress:
- A student may view only their own progress (
student_iddefaults to the caller). - Staff (super-admin, admin, content-manager, instructor) may view any student in their company by passing
student_id. - Otherwise →
403.
- A student may view only their own progress (
- Recompute (bulk) is staff-only →
403for students.
6. Endpoints
6.1 Training Progress (detailed)
Response
200
6.2 Trainings Progress (summary list)
Response
200 — standard paginator shape:
6.3 Quiz/Exam Progress (per-question breakdown)
Quiz model.
Query params
Response
200
reviewed_attemptisnullif the student has no attempts on this quiz.passed(insummary/attempts) is derived fromscore_percentage >= passing_percentage.
6.4 Quizzes/Exams Progress (summary list)
Response
200 — paginator with rows:
6.5 Gamification Score (recompute + rank)
company_id (✅), student_id (❌, staff only for others).
Response 200
rank is dense (1 = highest score in the company).
6.6 Leaderboard (top-N)
gamification_score (fast). To guarantee freshness, call recompute-scores first.
Query params
Response
200
me always reflects the caller’s standing, even if they are outside the top-N. Both the leaderboard rows and me.rank are scoped to the resolved role_id, so a staff caller’s me.rank is relative to the same role being listed (student by default).
6.7 Recompute Scores (bulk, staff)
gamification_score for every user in the company. Staff only (403 otherwise). Logged to history.
Body: company_id (✅).
Response 200
7. Per-question correctness model
The breakdown in §6.3 mirrors exactly howfinish_student_quiz stores answers in quiz_student_answers:
A question with no submitted answer is counted as incorrect.
8. Configuration
config/custom_config.php:
.env. Changing point values does not retro-edit stored scores — run recompute-scores afterwards to apply new weights.
Migration: 2026_06_08_000000_add_gamification_score_to_users_table.php adds the indexed users.gamification_score column (default 0). Run php artisan migrate.
9. History Tracking Actions
Read endpoints do not write history. Per-user
score recompute is a lightweight idempotent write and is intentionally not logged.
10. Related Files
11. Future Suggestions
These are deliberately not implemented yet — captured here for the planned “gamification award system”:- Real-time score updates. Today the score is recomputed on demand (
/score) or in bulk (/recompute-scores). To make the leaderboard live without a bulk job, add a small recompute hook in the two places completion happens:- after
LessonsController@toggle_lesson_validationattaches/detaches a lesson, callGamificationService::recomputeForUser($student); - after
save_student_quiz_resultpersists a passing score, do the same. These are additive (no behavioural change to the existing tracking) but were left out to honour the “do not touch the quiz/exam tracking” constraint. A model observer onQuizStudentAttemptis an alternative that keeps the controller untouched.
- after
- A score-event ledger (
gamification_score_events: user_id, source_type, source_id, points, awarded_at) for a full audit trail and “how did I earn this” UI, while keepingusers.gamification_scoreas the denormalized cache. Enables streaks, daily caps, and reversals. - Badges / achievements (
badges,badge_userpivot) — e.g. “First Course Completed”, “Perfect Exam”, “7-day streak”. The current score is the foundation; badges become rule evaluations over the same sources of truth. - Streaks — consecutive-day activity from
student_lesson.created_atand attempt timestamps; bonus points for maintaining streaks. - Cross-company / global leaderboard for super-admins (a
scope=globaloption), and time-windowed leaderboards (weekly/monthly) using the score-event ledger. - Configurable weights per company — move point values into
loyalty_points_settings-style per-company rows instead of global config, so each tenant tunes its own gamification. - Caching the leaderboard (e.g. 1–5 min cache) for high-traffic tenants, invalidated on
recompute-scores. - Completion certificates trigger — when
overall_progress_percentagehits 100, auto-suggest issuing the training certificate (ties into the existing certification module).