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.
Boot payload (19/10/2026):GET /auth/useralready carries, for a student, the per-training summary (my_trainings[].progress: lessons / quizzes done and total,overall_progress_percentage,is_completed,next_item) and the per-exam attempt summary (my_exams[].attempts) — the same rules as §6.2 / §6.4, computed set-based inApp\Services\AuthUserPayloadService. The/progress/*endpoints stay the detailed / staff view (per-chapter, per-question breakdowns, leaderboard).
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)
- 6.8 Overview (single trimmed snapshot)
- Real-time score updates (completion hooks)
gamification_scorein the auth-user payload- 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
chapters[].quizzes lists the chapter quizzes only; the training’s final quiz counts in summary.quizzes_total / quizzes_passed.
Errors
Fixed on 05/11/2026. Until then this endpoint answered
500 for every training that has a
quiz, with { "message": "The attribute [instructor_id] either does not exist or was not retrieved for model [App\\Models\\Quiz]." }
(the admin learner preview showed it as “The chapter tree could not be loaded”). The quiz query
selected five columns without instructor_id, while Quiz::$with = ['instructor'] eager-loads
the instructor, and Model::shouldBeStrict() refuses to read a column that was not selected. The
query now runs ->without('instructor') (the instructor is not used here). Guarded by
tests/Feature/TrainingProgressQuizzesTest.php.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.rank are scoped to the resolved role_id (students by default). me always reflects the caller’s standing within that role pool, even if they are outside the top-N — note that if the caller’s own role differs from role_id, their rank is computed relative to the requested role’s pool.
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
6.8 Overview (single trimmed snapshot)
company_id (✅), student_id (❌, staff only for others).
Response 200
trainingsare the student’s enrolled (owned-by-slug) trainings;quizzes_examsare only the ones they have attempted. For a single training’s chapter/lesson detail or a quiz’s per-question breakdown, drill in with §6.1 / §6.3.
7. Real-time score updates (completion hooks)
gamification_score stays fresh automatically — it is recomputed at the exact moments completion happens, so the leaderboard reflects activity immediately (no need to poll /score or run /recompute-scores for day-to-day use):
Both hooks call
GamificationService::recomputeForUser($student) and are wrapped in their own try/catch — a scoring hiccup is logged via report() and never breaks the underlying lesson-toggle or result-save response. The recompute is fully idempotent, so firing it on every toggle/save can never double-count.
These hooks are additive: they did not change the request/response contract or the stored data of the existing lesson and quiz/exam tracking endpoints. recompute-scores (§6.7) remains available for backfills or after changing point weights.
8. gamification_score in the auth-user payload
GET /api/auth/user (the auth_user endpoint, UserResource) now includes the caller’s score as a top-level field:
GET /api/progress/overview (§6.8); for deep detail use §6.1 / §6.3.
9. 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.
10. 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.
11. History Tracking Actions
Read endpoints do not write history. Per-user
score recompute is a lightweight idempotent write and is intentionally not logged.
12. Related Files
13. Future Suggestions
Captured here for the planned “gamification award system”:- ✅ Real-time score updates — IMPLEMENTED. The score is recomputed live at both completion points (lesson toggle + quiz/exam result save) — see §7. The on-demand (
/score) and bulk (/recompute-scores) paths remain for backfills and weight changes. (A model observer onQuizStudentAttemptremains a possible refactor if you ever want the hook to live outside the controller entirely.) - 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).