Skip to content

Student Engagement & Gamification

This document is the canonical developer reference for the Student Engagement & Gamification domain of the MAS / Mr. Mentor backend. It covers the four interlocking systems that drive the student dashboard's level ribbon and badges, the batch-lead "Students at Risk" card, the daily-card nudges, the warning automation, and the super-mentor rating / performance-metric pipeline. The domain is a mix of real-time XP hooks (fired inline when a student does something) and scheduled worker pipelines (risk scoring, badge safety-net evaluation, warning automation, daily cards) that roll those signals up into dashboards and outbound actions.

Status: documented from source on this branch.


Overview

The domain answers three questions for three audiences:

Audience Surface Question answered
Student (USER role) Dashboard level ribbon, badge wall, daily cards "How am I progressing, and what should I do next today?"
Batch Lead (BatchLead / batch-lead-gated) Students-at-Risk card "Which of my students are disengaging, why, and what action do I take?"
Super Mentor (EXPERT-ish, super-mentor-gated) Weekly ratings, skill ratings "How is each mentee performing on soft + technical skills, and how does that roll into their overall score?"

The systems and their source of truth:

System Drives Primary sources
XP / levels / streaks Student dashboard ribbon src/config/xpRules.ts, src/services/StudentProgressService.ts, src/entities/XpEvent.ts, src/entities/StudentProgress.ts
Badges Badge wall + celebration modal src/config/badges.config.ts, src/services/BadgeService.ts, src/entities/StudentBadge.ts
Student-at-Risk BL Students-at-Risk card + Miss Ozone framing src/config/riskRules.ts, src/services/StudentRiskService.ts, src/entities/StudentRiskScore.ts
Warnings automation Attendance warnings + 3-strike escalation src/services/WarningAutomationService.ts, src/entities/Warning.ts
Daily cards Student "today" nudges src/workers/daily-cards.worker.ts, src/entities/DailyCard.ts
Performance metrics Weighted student score src/services/PerformanceMetricService.ts, src/entities/PerformanceMetric.ts
Skill / weekly ratings Super-mentor scoring inputs src/entities/StudentSkillRating.ts, src/entities/WeeklyRating.ts

A detailed algorithm reference lives in-repo at docs/student-engagement-algorithms.md; this document summarizes the numbers and focuses on the data model, API surface, and end-to-end journeys.


Key concepts & entities

Glossary

  • XP event — an append-only ledger row for every point-granting action. Idempotent on (userId, type, sourceId).
  • Meaningful action — an XP event that is not a login, streak milestone, action milestone, or badge unlock. Counts toward one of the three level gates.
  • Level gate — a level L is held only when totalXp >= req.xp AND meaningfulActions >= req.actions AND longestStreak >= req.streak.
  • Streak — consecutive IST days with at least one event. Grace is exactly one missed day; a 2+ day gap resets to 1.
  • Blocking axis — the axis (xp / actions / streak) with the lowest percentage toward the next level; the dashboard highlights it.
  • Badge — a starter achievement keyed by a string id (first_step, week_warrior, ...). Unlock grants per-badge XP.
  • Risk score — composite 0-100 score per (student, batch), from five weighted signals; tiered low / medium / high.
  • Review suppression — when a BL marks a risk row reviewed (or a Miss Ozone call auto-reviews), the row is hidden for RISK_REVIEW_HOURS (48h).
  • Warning — a strike against a student (attendance / performance / payment / behavior / submission). The 3rd active warning escalates to the batch lead.
  • Daily card — a per-student, per-day nudge of one of three types (top priority / deadline alert / weak-area nudge), expiring after 24h.
  • Performance metric — a single weighted overall score per student from six components (eval test, learn progress, super-mentor rating, communication, attendance, mock interview).

Main TypeORM entities

Entity Table File Notes
XpEvent xp_events src/entities/XpEvent.ts Append-only ledger. UQ_XP_EVENT_DEDUP (userId, type, sourceId). Stores amount, baseXp, multipliersApplied, occurredAt.
StudentProgress student_progress src/entities/StudentProgress.ts Cached O(1) snapshot. PK userId. totalXp, meaningfulActions, level, currentStreak, longestStreak, optimistic version.
StudentBadge student_badges src/entities/StudentBadge.ts One row per (userId, badgeId). UQ_STUDENT_BADGE. seenAt null until celebration modal shown.
StudentRiskScore student_risk_scores src/entities/StudentRiskScore.ts Latest-only per (studentId, batchId). UQ_STUDENT_RISK_SCORE. signals jsonb, tier, reason, reasonSource, reviewedUntil, lastCallSummary.
Warning warnings src/entities/Warning.ts WarningType + WarningSeverity enums. isActive, escalatedToBatchLead, resolvedAt.
DailyCard daily_cards src/entities/DailyCard.ts DailyCardType enum. priority, expiresAt, dismissed, ctaLabel/ctaLink.
PerformanceMetric performance_metrics src/entities/PerformanceMetric.ts Six component scores + weighted overallScore.
StudentSkillRating student_skill_ratings src/entities/StudentSkillRating.ts Per (superMentorId, studentId, skillName). Beginner/intermediate/advanced + overallRating (0-100) + smRating (0-5).
WeeklyRating weekly_ratings src/entities/WeeklyRating.ts Unique (studentId, weekNumber, year). Five 0-5 sub-ratings + overallAverage.

Architecture

flowchart TD
  subgraph Actors["Actors"]
    STU["Student (USER)"]
    BL["Batch Lead"]
    SM["Super Mentor"]
  end

  subgraph Routes["Express routes"]
    SR["/api/student/me/* progress + badges"]
    DCR["/api/student/daily-cards"]
    BLR["/api/batchlead/risk-students/*"]
    BLW["/api/batchlead/students/:id/warnings"]
    SMR["/api/supermentor/students/:id/ratings + skills"]
  end

  subgraph Ctrls["Controllers"]
    SPC["StudentProgressController"]
    NC["NotificationController (daily cards)"]
    SRC["StudentRiskController"]
    BLC["BatchLeadController"]
    SMC["SuperMentorController"]
  end

  subgraph Svcs["Services"]
    SPS["StudentProgressService grantXp"]
    BS["BadgeService"]
    SRS["StudentRiskService"]
    WAS["WarningAutomationService"]
    PMS["PerformanceMetricService"]
  end

  subgraph Hooks["Inline XP hooks"]
    QZ["studentQuiz.controller"]
    SLOT["SlotCompletionService"]
    MLS["MrLearnSyncService"]
    MTS["MrTestSyncService"]
  end

  subgraph Workers["BullMQ workers (scheduled)"]
    BEW["badgeEvaluation.worker 24h"]
    SRW["studentRiskComputation.worker 24h"]
    WW["warning.worker 23:59 IST"]
    DCW["daily-cards.worker 00:00 IST"]
  end

  subgraph Data["PostgreSQL + Redis"]
    XPE[("xp_events")]
    SP[("student_progress")]
    SB[("student_badges")]
    RSK[("student_risk_scores")]
    WRN[("warnings")]
    DC[("daily_cards")]
    PM[("performance_metrics")]
    RED[("Redis daily-cards cache")]
  end

  subgraph Ext["External"]
    GROQ["Groq LLM (risk reason + AI recs)"]
    OZONE["Miss Ozone LiveKit voice agent"]
  end

  STU --> SR --> SPC --> SPS
  SPC --> BS
  STU --> DCR --> NC
  BL --> BLR --> SRC --> SRS
  BL --> BLW --> BLC --> WAS
  SM --> SMR --> SMC --> PMS

  QZ --> SPS
  SLOT --> SPS
  MLS --> SPS
  MTS --> SPS
  QZ --> BS
  SLOT --> BS

  SPS --> XPE
  SPS --> SP
  BS --> SB
  BEW --> BS
  SRW --> SRS --> RSK
  WW --> WAS --> WRN
  DCW --> DC
  DCW --> RED
  PMS --> PM

  SRS --> GROQ
  SRS --> OZONE
  NC --> RED

Data model

erDiagram
  USER ||--o| STUDENT_PROGRESS : "has snapshot"
  USER ||--o{ XP_EVENT : "earns"
  USER ||--o{ STUDENT_BADGE : "unlocks"
  USER ||--o{ STUDENT_RISK_SCORE : "scored per batch"
  USER ||--o{ WARNING : "receives"
  USER ||--o{ DAILY_CARD : "is nudged by"
  USER ||--o{ PERFORMANCE_METRIC : "measured by"
  USER ||--o{ WEEKLY_RATING : "rated weekly"
  USER ||--o{ STUDENT_SKILL_RATING : "rated per skill"

  STUDENT_PROGRESS {
    uuid userId PK
    int totalXp
    int meaningfulActions
    smallint level
    int currentStreak
    int longestStreak
    date lastActiveDate
    int version
  }
  XP_EVENT {
    uuid id PK
    uuid userId FK
    varchar type
    int amount
    int baseXp
    varchar sourceId
    timestamp occurredAt
    jsonb multipliersApplied
  }
  STUDENT_BADGE {
    uuid id PK
    uuid userId FK
    varchar badgeId
    timestamp unlockedAt
    timestamp seenAt
  }
  STUDENT_RISK_SCORE {
    uuid id PK
    uuid studentId FK
    uuid batchId
    smallint score
    varchar tier
    jsonb signals
    text reason
    varchar reasonSource
    timestamp reviewedUntil
    text lastCallSummary
    timestamp computedAt
  }
  WARNING {
    uuid id PK
    uuid studentId FK
    enum type
    enum severity
    text description
    boolean isActive
    boolean escalatedToBatchLead
    timestamp resolvedAt
  }
  DAILY_CARD {
    uuid id PK
    uuid userId FK
    enum cardType
    varchar title
    int priority
    timestamp expiresAt
    boolean dismissed
  }
  PERFORMANCE_METRIC {
    uuid id PK
    uuid studentId FK
    decimal evalTestScore
    decimal learnProgressScore
    decimal superMentorRating
    decimal communicationScore
    decimal attendanceScore
    decimal mockInterviewScore
    decimal overallScore
  }
  WEEKLY_RATING {
    uuid id PK
    uuid studentId FK
    uuid superMentorId
    int weekNumber
    int year
    decimal overallAverage
  }
  STUDENT_SKILL_RATING {
    uuid id PK
    uuid studentId FK
    uuid superMentorId
    varchar skillName
    decimal overallRating
    decimal smRating
  }

Notable enums / status fields

  • RiskTier = 'low' | 'medium' | 'high' (src/entities/StudentRiskScore.ts).
  • reasonSource = 'ai' | 'template' — whether the risk reason came from Groq or the deterministic fallback.
  • WarningType = attendance | performance | payment | behavior | submission; WarningSeverity = low | medium | high | critical (src/entities/Warning.ts).
  • DailyCardType = top_priority | deadline_alert | weak_area_nudge (src/entities/DailyCard.ts).
  • XP event type values: daily_login, assignment_submitted, test_taken, module_completed, mentor_call_completed, streak_milestone, action_milestone, badge_unlocked, tenure_bonus. Non-action types are excluded from the meaningful-actions gate.

API surface

All paths below are derived from the actual route files. Mount prefixes (from src/routes/index.ts): student routes mount at /api/student; batch-lead routes at /api/batchlead; super-mentor routes at /api/supermentor.

Student-facing (src/routes/student.routes.ts)

Method Path Auth/role Purpose
GET /api/student/me/progress authMiddleware (any authed user) Level ribbon snapshot: level, totalXp, three axes, blockingAxis, unseenBadges.
GET /api/student/me/badges authMiddleware Full badge wall (all starter badges with unlocked/seen flags).
POST /api/student/me/badges/seen authMiddleware Clears the unseen flag for all badges (after celebration modal).
GET /api/student/daily-cards authMiddleware Today's non-dismissed, non-expired daily cards.
POST /api/student/daily-cards/:id/dismiss authMiddleware Dismiss one daily card.

Batch-lead facing (src/routes/batchLead.routes.ts, gated by authMiddleware + batchLeadMiddleware)

Method Path Auth/role Purpose
GET /api/batchlead/risk-students Batch Lead Top N actionable at-risk students for this BL's batches (limit 1-20, default 5).
POST /api/batchlead/risk-students/recompute Batch Lead On-demand recompute of risk scores across all of this BL's batches.
POST /api/batchlead/risk-students/:id/review Batch Lead Mark a risk row reviewed, suppressing it for RISK_REVIEW_HOURS (48h).
POST /api/batchlead/risk-students/:id/miss-ozone Batch Lead Fire a Miss Ozone outbound check-in call for the student.
POST /api/batchlead/risk-students/:id/ai-classroom Batch Lead File an AI Classroom request for the at-risk student.
GET /api/batchlead/students/:id/warnings Batch Lead List a student's warnings.
POST /api/batchlead/students/:id/warnings Batch Lead Create a manual warning.
PATCH /api/batchlead/warnings/:warningId/resolve Batch Lead Resolve a warning.
GET /api/batchlead/warnings/escalated Batch Lead List students escalated (3 active warnings).
GET /api/batchlead/students/:id/skills Batch Lead Read a student's skill ratings.
PATCH /api/batchlead/students/:id/skills Batch Lead Update skill ratings.

Super-mentor facing (src/routes/superMentor.routes.ts)

Method Path Auth/role Purpose
GET /api/supermentor/students/ratings/week Super Mentor Weekly ratings for all mentees for a given week.
GET /api/supermentor/students/:id/ratings Super Mentor A mentee's weekly rating for the current week.
GET /api/supermentor/students/:id/ratings/history Super Mentor Weekly rating history.
POST /api/supermentor/students/:id/ratings Super Mentor Save/update a weekly rating (feeds the super-mentor performance component).
GET /api/supermentor/students/:id/skills Super Mentor Read skill ratings.
PATCH /api/supermentor/students/:id/skills Super Mentor Update skill ratings.
POST /api/supermentor/students/:id/warnings Super Mentor Issue a warning.
PATCH /api/supermentor/warnings/:warningId/resolve Super Mentor Resolve a warning.

Per-request ownership is re-checked inside the services (e.g. StudentRiskService.batchesLedBy + a "Not your student" guard) even though routes are role-gated upstream.


User journeys

Journey 1 — XP event grant then level / streak update

A student takes an action (submits an assignment, takes a test, completes a module, finishes a mentor call). The originating controller/service calls StudentProgressService.grantXp(userId, type, sourceId, ctx). The grant runs in a single DB transaction that locks the student_progress row, computes multipliers, inserts the ledger event, and recomputes the cached level/streak. The unique key (userId, type, sourceId) makes the whole thing idempotent — replays no-op.

sequenceDiagram
  participant SRC as Quiz or Slot or Sync hook
  participant SPS as StudentProgressService
  participant DB as PostgreSQL
  participant BS as BadgeService

  SRC->>SPS: grantXp userId type sourceId ctx
  SPS->>DB: BEGIN transaction
  SPS->>DB: check xp_events for userId type sourceId
  alt event already exists
    DB-->>SPS: row found
    SPS-->>SRC: return null no-op idempotent
  else new event
    SPS->>DB: SELECT student_progress FOR UPDATE lock row
    SPS->>SPS: count recent same-type events for anti-farm
    SPS->>SPS: check first meaningful event of IST day
    SPS->>SPS: computeMultipliers streak quality timing freshness anti-farm clamp 0.5 to 2.0
    SPS->>DB: INSERT xp_events amount base multipliers
    SPS->>SPS: walk streak using IST date diff one-day grace
    SPS->>SPS: recompute level from xp actions longestStreak gates
    SPS->>DB: UPDATE student_progress totals streak level version
    opt streak hit multiple of 7
      SPS->>DB: recursive grant streak_milestone plus 50 XP skipStreakUpdate
    end
    SPS->>DB: COMMIT
    SPS-->>SRC: GrantResult with event and leveledUp
    SRC->>BS: evaluateForUser userId non-blocking
  end

Key behaviors (src/services/StudentProgressService.ts): - Idempotency — pre-check plus the DB unique constraint as the final defense against races. - Anti-farm uses createdAt (grant time), not occurredAt, so backfills do not poison the live 24h counter. - Backfill safety — historical events must pass skipStreakUpdate: true or the streak engine rewinds lastActiveDate.

Journey 2 — Student reads the dashboard ribbon

sequenceDiagram
  participant FE as Student dashboard
  participant SPC as StudentProgressController
  participant SPS as StudentProgressService
  participant BS as BadgeService
  participant DB as PostgreSQL

  FE->>SPC: GET /api/student/me/progress
  SPC->>SPS: getProgress userId
  SPS->>DB: SELECT student_progress
  alt no row yet
    DB-->>SPS: none
    SPS-->>SPC: synthesized zero snapshot level 1
  else found
    DB-->>SPS: snapshot
  end
  SPC->>SPC: requirementForLevel current and next
  SPC->>SPC: compute three axes percent and pick lowest as blockingAxis
  SPC->>BS: listUnseenBadgeIds userId
  BS->>DB: SELECT student_badges where seenAt is null
  DB-->>BS: unseen ids
  SPC-->>FE: level axes overallPercent blockingAxis unseenBadges

The dashboard renders three axis bars; blockingAxis tells the UI which "X more to unlock L_n" line to highlight. isOnFireToday is true when lastActiveDate === today in IST.

Journey 3 — Badge unlock (inline hook + safety-net cron)

Badges unlock two ways: (1) inline after an XP-granting action via BadgeService.evaluateForUser, and (2) the daily badgeEvaluation.worker safety net (mainly so profile_pro, whose source does not pass through grantXp, still unlocks). Unlock is idempotent on (userId, badgeId) and credits the per-badge XP reward via the normal grantXp path.

sequenceDiagram
  participant TRG as Inline hook or 24h cron
  participant BS as BadgeService
  participant DB as PostgreSQL
  participant SPS as StudentProgressService

  TRG->>BS: evaluateForUser userId
  BS->>DB: SELECT already-earned badge ids
  loop each starter badge not yet earned
    BS->>BS: run evaluator first_step week_warrior test_taker module_master etc
    alt criteria met
      BS->>DB: pre-check student_badges for userId badgeId
      alt not present
        BS->>DB: INSERT student_badges defer to UQ_STUDENT_BADGE
        BS->>SPS: grantXp badge_unlocked badgeId baseOverride xpReward
        SPS->>DB: ledger plus progress update
        BS->>BS: append id to newlyUnlocked
      else duplicate
        BS->>BS: skip no double-award
      end
    end
  end
  BS-->>TRG: newlyUnlocked ids

The student later calls GET /api/student/me/badges; freshly unlocked ids show up in unseenBadges on the progress endpoint, the UI shows the celebration modal, then calls POST /api/student/me/badges/seen to clear the flag. Test/module badges are batch-scoped — only Mr Test exams and Mr Learn courses attached to the student's batch count.

Journey 4 — Daily risk computation then BL action

The studentRiskComputation.worker runs every 24h, walking active batches and writing one student_risk_scores row per enrolled student (five weighted signals to a 0-100 composite, tiered). For each row it tries Groq for a one-sentence reason and falls back to a deterministic template. The BL then reads the card and acts.

sequenceDiagram
  participant CRON as studentRiskComputation worker 24h
  participant SRS as StudentRiskService
  participant DB as PostgreSQL
  participant GROQ as Groq LLM
  participant BL as Batch Lead
  participant OZ as Miss Ozone pipeline

  CRON->>SRS: runDailyComputation
  loop each active batch and enrolled student
    SRS->>DB: gather logins learn-progress tests warnings placement
    SRS->>SRS: compute five signals weight to composite and tier
    SRS->>SRS: skip new students within 7-day grace
    SRS->>GROQ: generate one-sentence reason
    alt Groq ok
      GROQ-->>SRS: AI reason reasonSource ai
    else any failure
      SRS->>SRS: deterministic template reasonSource template
    end
    SRS->>DB: UPSERT student_risk_scores on studentId batchId
  end
  SRS-->>CRON: batchesProcessed studentsProcessed flagged

  BL->>SRS: GET /api/batchlead/risk-students
  SRS->>DB: top rows where reviewedUntil passed score desc
  DB-->>SRS: rows
  SRS-->>BL: at-risk list with reasons
  alt BL marks reviewed
    BL->>SRS: POST risk-students id review
    SRS->>DB: set reviewedAt and reviewedUntil now plus 48h
  else BL triggers Miss Ozone
    BL->>SRS: POST risk-students id miss-ozone
    SRS->>OZ: triggerMissOzone writes call row state scheduled
    Note over OZ: voice agent calls student then analyzer summarizes
    OZ->>SRS: attachCallSummary studentId batchId summary
    SRS->>DB: write lastCallSummary plus auto-review 48h reviewedBy null
  end

Gates and grace (src/config/riskRules.ts, src/services/StudentRiskService.ts): - 7-day new-student grace — applications created within the last 7 days are skipped. - 48h review suppression (RISK_REVIEW_HOURS) — reviewed/called rows drop off the card, except rows carrying a lastCallSummary which stay visible so the BL can read it. - App-status filter — only enrolled / batch_allocated / paid applications are scored.

Journey 5 — Daily warning automation + 3-strike escalation

The warning.worker fires at 23:59 IST. WarningAutomationService.processDailyAbsences finds yesterday's ABSENT attendance records and creates one attendance warning per absent student, deduping on description-date and skipping students who already have 3 active warnings. The 3rd warning escalates to the batch lead.

sequenceDiagram
  participant CRON as warning worker 23:59 IST
  participant WAS as WarningAutomationService
  participant DB as PostgreSQL
  participant BL as Batch Lead dashboard

  CRON->>WAS: processDailyAbsences yesterday
  WAS->>DB: find ABSENT attendance for target date
  WAS->>DB: load existing active attendance warnings for those students
  loop each absent student
    alt already warned for this date or already 3 warnings
      WAS->>WAS: skip increment skipped
    else
      WAS->>DB: INSERT warning attendance medium createdBy SYSTEM
      WAS->>DB: increment user activeWarningCount
      alt count reaches 3
        WAS->>DB: set user escalatedToBatchLead and escalatedAt
        WAS->>DB: mark warning escalatedToBatchLead
        WAS->>WAS: increment studentsEscalated
      end
    end
  end
  WAS-->>CRON: processed warningsCreated studentsEscalated skipped errors
  BL->>BL: GET /api/batchlead/warnings/escalated shows 3-strike students

Batch leads and super mentors can also create warnings manually via the warning endpoints; resolving a warning flips isActive and stamps resolvedAt / resolvedBy.

Journey 6 — Daily card generation + delivery

The daily-cards.worker runs at midnight IST. For every enrolled student it generates up to three cards (top priority, deadline alert, weak-area nudge), deletes the prior day's cards, persists the new set, and caches them in Redis with a 24h TTL. The student fetches them and can dismiss individual cards.

sequenceDiagram
  participant CRON as daily-cards worker 00:00 IST
  participant DB as PostgreSQL
  participant RED as Redis
  participant FE as Student dashboard
  participant NC as NotificationController

  CRON->>DB: distinct enrolled userIds enrollment plus active applications
  loop each student
    CRON->>DB: latest perf recent quizzes upcoming assignments ai-classroom progress
    CRON->>CRON: build top_priority deadline_alert weak_area_nudge cards
    CRON->>DB: DELETE old daily_cards then INSERT new set
    CRON->>RED: SET daily-cards student key TTL 86400
  end

  FE->>NC: GET /api/student/daily-cards
  NC->>DB: non-dismissed non-expired cards for user
  NC-->>FE: today cards
  FE->>NC: POST daily-cards id dismiss
  NC->>DB: set dismissed true

Card selection logic (src/workers/daily-cards.worker.ts): top-priority prefers an upcoming assignment, else a recent quiz under 50%, else an in-progress AI classroom, else a generic explore card. Weak-area nudge picks the lowest of eval-test / learn-progress / communication / attendance scores from the latest performance_metrics row.

Journey 7 — Super-mentor rating then performance-metric roll-up

A super mentor saves a weekly rating or skill rating. PerformanceMetricService recomputes the affected component(s) and the weighted overall score. The super-mentor component is 50% weekly ratings + 50% skill ratings, normalized and stored as a 0-5 value, then weighted 30% into the overall.

sequenceDiagram
  participant SM as Super Mentor
  participant SMC as SuperMentorController
  participant DB as PostgreSQL
  participant PMS as PerformanceMetricService

  SM->>SMC: POST /api/supermentor/students/:id/ratings weekly sub-ratings
  SMC->>DB: UPSERT weekly_ratings overallAverage
  SMC->>PMS: updateSuperMentorScore studentId freshWeeklyValue
  PMS->>DB: load skill_ratings overallRating
  PMS->>PMS: combine weekly 50pct plus skill 50pct then divide by 20 for 0-5
  PMS->>DB: getOrCreate performance_metrics set superMentorRating
  PMS->>PMS: calculateOverallScore weighted sum six components
  PMS->>DB: save overallScore
  PMS-->>SMC: done
  SMC-->>SM: saved

Overall-score weights (PerformanceMetricService.calculateOverallScore): eval test 15%, learn progress 15%, super-mentor rating 30% (the 0-5 value normalized x20), communication 15% (currently a not-implemented placeholder, always 0), attendance 15%, mock interview 10%. Component updaters (updateEvalTestScore, updateAttendanceScore, updateMockInterviewScore, etc.) are called inline when source data changes.


Background jobs & async

Worker Queue Schedule Source Purpose
badgeEvaluation.worker badgeEvaluationQueue every 24h (scheduleBadgeEvaluation(24), repeat.every) src/workers/badgeEvaluation.worker.ts Pages enrolled students (200/page) and re-runs evaluateForUser as a safety net.
studentRiskComputation.worker studentRiskComputationQueue every 24h (scheduleStudentRiskComputation(24), stable jobId student-risk-daily) src/workers/studentRiskComputation.worker.ts Recomputes student_risk_scores for all active batches/students.
warning.worker warningQueue cron 59 23 * * * (11:59 PM daily) src/workers/warning.worker.ts Processes yesterday's absences into attendance warnings + escalations.
daily-cards.worker dailyCardsQueue cron 0 0 * * * tz Asia/Kolkata (midnight IST) src/workers/daily-cards.worker.ts Generates + caches three daily cards per enrolled student.

Scheduling is wired in src/index.ts (worker imports + queueService.schedule* calls) and the schedule definitions live in src/services/QueueService.ts. All four workers run concurrency: 1 — the work iterates internally, so queue-level parallelism is unnecessary. Repeatable jobs use stable jobIds / clear-then-re-add so a reboot replaces rather than stacks schedules.

Related queues (adjacent, not owned here): the KPI worker (kpiQueue, every 15 min) computes dashboard KPIs; Miss Ozone has its own reconciler (scheduleMissOzonePolling, every 60s) and retention prune (daily 03:30) that interact with risk rows via attachCallSummary.

No webhooks are owned directly by this domain; the inbound async surface is the Miss Ozone evaluation fetch (BullMQ job) which calls back into StudentRiskService.attachCallSummary. There are no domain-specific Socket.IO events.

Inline (non-worker) XP/badge hooksgrantXp + evaluateForUser are also called synchronously (fire-and-forget, non-blocking) from: studentQuiz.controller.ts (assignment submission), SlotCompletionService.ts (mentor call completed), MrLearnSyncService.ts (module completed), MrTestSyncService.ts (test taken), and auth.controller.ts / DemoSeedService.ts.


External integrations

Integration Where Env / config Failure / fallback
Groq LLM (llama-3.3-70b-versatile) Risk-row reason generation + AI recommendations (StudentRiskService, AiStudentProfileService) GROQ_API_KEY; DEBUG_AI_RECS=1 for verbose logs Fails soft: no key / timeout (~4.5s) / non-200 / malformed JSON / multiline / over length all fall back to the deterministic template (reasonSource = 'template') or [] recs. The algo path is the floor.
Miss Ozone voice agent (LiveKit + Twilio SIP) Outbound at-risk check-in calls (MissOzoneService, triggered from StudentRiskService.triggerMissOzone) LiveKit / Twilio config (see Miss Ozone docs) If the student does not pick up the call goes to failed with no auto-review; the dedupe gate stops blocking re-triggers.
Redis Daily-card cache (24h TTL) and all BullMQ queues REDIS_HOST, REDIS_PORT Cache miss falls back to the DB read in NotificationController.getDailyCards.
Mr Learn / Mr Test (Graphy + EzExam) Source of module_completed / test_taken XP and risk signals; batch-scoping for badges sync worker configs Email-matched (prefix-tolerant for Graphy's <orgPrefix>-<email>).

There is no dedicated feature flag for the gamification systems; they are always on. Tuning constants live in src/config/xpRules.ts, src/config/badges.config.ts, and src/config/riskRules.ts.


Status lifecycles

Risk-row review state

A student_risk_scores row cycles between visible-on-card and suppressed. The composite score/tier is recomputed daily; the review state controls visibility independent of score.

stateDiagram-v2
  [*] --> Computed
  Computed --> Visible : tier medium or high and reviewedUntil passed
  Computed --> Hidden : tier low below 40
  Visible --> Suppressed : BL marks reviewed sets reviewedUntil now plus 48h
  Visible --> Called : BL triggers Miss Ozone
  Called --> SummaryAttached : call wraps with analyzer summary auto-review 48h
  Called --> Visible : student not picked up failed no auto-review
  SummaryAttached --> Visible : 48h elapses still flagged shows Call again
  Suppressed --> Visible : 48h elapses and still at risk on next cron
  Hidden --> Visible : signals worsen on next cron

Warning lifecycle

stateDiagram-v2
  [*] --> Active : created manual or SYSTEM auto-absence
  Active --> Escalated : student reaches 3 active warnings
  Active --> Resolved : BL or SM resolves sets resolvedAt resolvedBy
  Escalated --> Resolved : warning resolved
  Resolved --> [*]

Daily-card lifecycle

stateDiagram-v2
  [*] --> Generated : midnight IST worker creates card expiresAt now plus 24h
  Generated --> Dismissed : student dismisses
  Generated --> Expired : expiresAt passes
  Generated --> Replaced : next midnight run deletes and regenerates
  Dismissed --> [*]
  Expired --> [*]
  Replaced --> [*]

Badge lifecycle

stateDiagram-v2
  [*] --> Locked
  Locked --> Unlocked : criteria met insert row grant xp reward
  Unlocked --> Seen : student views celebration then POST badges seen
  Seen --> [*]

Edge cases, limits & gotchas

  • Idempotency everywhere. XP grants dedupe on (userId, type, sourceId); badge unlocks on (userId, badgeId); risk rows upsert on (studentId, batchId). Cron + real-time evaluators can both fire without double-counting.
  • Streak uses longestStreak for level gates (not currentStreak) so a student who once hit 7 days but broke the run is not demoted.
  • Backfill streak hazard. Historical XP replays MUST pass skipStreakUpdate: true or the streak engine rewinds lastActiveDate. src/scripts/repairStreaks.ts exists to clean up rows clobbered by this.
  • Anti-farm uses grant time, not occurredAt. Submitting five tests in a row earns less per test; backfills don't poison the live 24h counter.
  • Multiplier clamp. The product of all multipliers is clamped to [0.5, 2.0] so a 100-day streak student doesn't lap the cohort.
  • Route prefix mismatch. The risk controller doc-comment says /api/batch-lead, but the actual mount in src/routes/index.ts is /api/batchlead (no hyphen). Use /api/batchlead/risk-students. Express requires risk-students/recompute to be declared before the dynamic :id routes so "recompute" is not parsed as a student id.
  • markReviewed returns 404 when no risk row exists yet for the student — the BL must wait for (or trigger) a cron run first. Ownership failures ("Not your student") return 403.
  • reviewedBy is null on Miss Ozone auto-review — that's how the system distinguishes automated review from a BL clicking "Mark Reviewed".
  • Communication score is a placeholderPerformanceMetricService.updateCommunicationScore always sets 0 (15% weight currently dead). Documented TODO in source.
  • zeroLearn penalizes missing learner rows — returns 100 when the batch has courses attached but the student has no learner row (they should have one).
  • Badge batch-scopingtest_taker and module_master only count Mr Test exams / Mr Learn courses attached to the student's enrolled batch, matched by email with Graphy prefix tolerance.
  • Daily cards are fully replaced each runcardRepo.delete({ userId }) then insert; dismissals do not persist across days.
  • Performance metric reads use latest-by-calculatedAtgetOrCreateMetric finds the most recent row or creates a zeroed one; there is no single canonical row, so component updaters all write to the latest.
  • Multi-platform — these endpoints are not platform-branched; they key entirely off the authenticated userId / batch membership, independent of the x-platform header.

  • Mentorship & Meetings — mentor calls feed mentor_call_completed XP.
  • Token Economy — mock-interview tokens feed the performance metric's mock score.
  • Identity & Access — roles (USER, batch lead, super mentor) and auth middleware that gate these routes.
  • Mr. Hire — Voice Interviews & AI Calling — sibling voice-agent pipeline (Miss Ozone shares the LiveKit/voice pattern).
  • In-repo algorithm reference: mr-mentor-backend/docs/student-engagement-algorithms.md — exact constants, Miss Ozone prompt template, and operational scripts.