Skip to content

Integration — Mr. Test (External Exam Sync)

This domain integrates the MAS backend with Mr. Test — an external online-exam platform built on EzExam (a Django app reachable at https://myanalyticsschool.ezexam.in). Because EzExam exposes no public API and no JSON read-models for most surfaces, the backend logs in as a staff user, scrapes/calls EzExam's internal /staff/* and /students/* endpoints, and caches a snapshot of online exams, students, submissions, and per-student analysis into a dedicated mrtest Postgres schema. A BullMQ worker keeps that snapshot fresh on a per-exam schedule, an HTML parser distills EzExam's server-rendered analysis page into structured diagnostics, and the cached data is reused to credit student XP, drive admin dashboards, gate batch test launches, and assemble AI-training datasets.

Status: documented from source on this branch.


Overview

Mr. Test is the assessment arm of the MAS suite. Students sit timed exams on EzExam; MAS needs that performance data inside its own platform to (a) show admins synced results, (b) reward students with engagement XP and badges, (c) gate course-roadmap tests behind Mr. Learn prerequisites, and (d) feed a per-question dataset to AI tooling.

The integration is read-mostly and one-directional (EzExam → MAS), with two write-back exceptions: pushing MAS students into EzExam ("Create Student") and minting MAS accounts from EzExam student rows. All EzExam access flows through a single shared staff session, so the sync worker runs strictly serial to avoid racing EzExam's rotating CSRF token.

Who uses it

Persona What they do
Admin / Superadmin Configure EzExam credentials, create & run sync configs (per-exam, per-batch, per-tag), browse cached exams/submissions/students, mint MAS accounts, map exams to batch roadmaps. All /api/ezexam/* routes require ADMIN/SUPERADMIN.
Student Indirectly: launches a batch-mapped Mr. Test exam via the student dashboard (/api/student/mrtest-exams/:id/launch), earns XP/badges when their submission is synced, receives a "your score is in" notification.
Sync worker Background BullMQ worker that runs runSyncForConfig on schedule, on manual trigger, and on boot.
AI / research tooling Pulls the per-question training dataset and parsed analysis blobs.

Where it sits: this is a peer of the Mr. Learn integration (Graphy LMS) — both are external-LMS sync engines living under the same admin area. Mr. Test cross-links into the LMS (batch roadmaps, prerequisite courses), the engagement system (XP/badges/notifications), and identity (account minting).


Key concepts & entities

Glossary

  • EzExam — the upstream Django exam platform. The MAS-facing brand name is "Mr. Test". Base URL from EZEXAM_BASE_URL.
  • onex / online exam — one scheduled exam instance (EzExam onexs[].id). The unit MAS syncs against.
  • qpap / question paper — the reusable paper (qpaps[].Id) an onex references. Carries { Name, Date, Subjects, Type, mins, status, crby }.
  • qexam — EzExam's paginated catalog row for an exam; carries swith (student-tags) and surl (direct take URL).
  • swith / tags / groups — EzExam's student-tag labels (e.g. MAS101-2025). Used for "sync by tag" and the sidebar filter.
  • rollnum — EzExam student roll number. The only stable identifier shared with MAS in many exam types; submission and result rows often omit email.
  • SyncConfig — one row per exam MAS wants to keep synced; holds the schedule + last-run status.
  • SyncRun — an immutable audit record of one execution of a sync config.
  • Detailed answers — optional per-question payloads (subjective + OMR responses + parsed analysis HTML). Slower; opt-in per config.
  • Training data — on-demand, expensive per-question join (question + answer-key + the student's response) used for AI.

Entities (all under the mrtest Postgres schema unless noted)

Entity File Table Role
MrTestAuthCredentials src/entities/mrtest/MrTestAuthCredentials.ts mrtest.auth_credentials EzExam staff username + AES-256-GCM-encrypted password + cached Django session/CSRF cookies.
MrTestSyncConfig src/entities/mrtest/MrTestSyncConfig.ts mrtest.sync_configs One per synced exam: schedule, enabled flag, last-run status, optional batchId/tags. Unique on onlineExamId.
MrTestSyncRun src/entities/mrtest/MrTestSyncRun.ts mrtest.sync_runs Audit log of each run: counts, duration, status, error.
MrTestOnlineExam src/entities/mrtest/MrTestOnlineExam.ts mrtest.online_exams Cached snapshot of an onex + folded question-paper metadata + the many EzExam boolean flags. PK = onex id.
MrTestSubmission src/entities/mrtest/MrTestSubmission.ts mrtest.submissions One student's result for one exam. Unique on (onlineExamId, ezExamUserId).
MrTestSubmissionReport src/entities/mrtest/MrTestSubmissionReport.ts mrtest.submission_reports Detailed per-answer blob (subjective + OMR + parsed analysis). Populated only when includeDetailedAnswers. Unique on (onlineExamId, ezExamUserId).
MrTestStudent src/entities/mrtest/MrTestStudent.ts mrtest.students Deduped roster — one row per ezExamUserId, refreshed as a side-effect of every submission sync.
BatchMrTestExam src/entities/BatchMrTestExam.ts batch_mrtest_exams (default schema) Maps an exam to a batch roadmap item, with optional Mr. Learn prerequisite gating + a stored take URL.

Architecture

flowchart TD
  subgraph Client["Admin & Student UIs"]
    AUI["Admin mrtest pages"]
    SUI["Student dashboard"]
  end

  subgraph Routes["Express routes"]
    EZR["/api/ezexam/* (EzExamRoutes)"]
    ADMR["/api/admin/mas/batches/:id/mrtest-exams"]
    STR["/api/student/mrtest-exams/:id/launch"]
  end

  subgraph Controllers["Controllers"]
    SYC["MrTestSyncController"]
    EZC["EzExamController"]
    AMC["AdminMasController"]
    STC["StudentController"]
  end

  subgraph Services["Services"]
    SYS["MrTestSyncService"]
    TDS["MrTestTrainingDataService"]
    EZS["EzExamService (HTTP client)"]
    BSV["BatchService"]
    QS["QueueService"]
    SPS["StudentProgressService + BadgeService"]
    NTS["NotificationService"]
  end

  subgraph Async["Background"]
    Q["mrtestSyncQueue (BullMQ)"]
    W["mrtestSync.worker"]
  end

  subgraph DB["Postgres schema mrtest + default"]
    CFG["sync_configs"]
    RUN["sync_runs"]
    OEX["online_exams"]
    SUB["submissions"]
    REP["submission_reports"]
    STU["students"]
    AUTH["auth_credentials"]
    BME["batch_mrtest_exams"]
  end

  EXT["EzExam / Mr Test (Django) at EZEXAM_BASE_URL"]

  AUI --> EZR --> SYC
  AUI --> EZR --> EZC
  AUI --> ADMR --> AMC
  SUI --> STR --> STC

  SYC --> QS --> Q --> W --> SYS
  SYC --> SYS
  SYC --> TDS
  EZC --> EZS
  AMC --> BSV
  STC --> BSV

  SYS --> EZS
  TDS --> EZS
  EZS --> AUTH
  EZS --> EXT

  SYS --> OEX
  SYS --> SUB
  SYS --> REP
  SYS --> STU
  SYS --> RUN
  SYS --> CFG
  SYS --> SPS
  SYS --> NTS
  BSV --> BME

Data model

erDiagram
  SYNC_CONFIG ||--o{ SYNC_RUN : "audited by"
  SYNC_CONFIG ||--o{ SUBMISSION : "produces"
  ONLINE_EXAM ||--o{ SUBMISSION : "has"
  ONLINE_EXAM ||--o{ SUBMISSION_REPORT : "has"
  SUBMISSION }o--|| STUDENT : "dedupes to"
  BATCH_MRTEST_EXAM }o--|| ONLINE_EXAM : "references"

  AUTH_CREDENTIALS {
    uuid id PK
    string username UK
    text encryptedPassword
    text sessionId
    text csrfToken
    text csrfFormToken
    timestamp authenticatedAt
  }

  SYNC_CONFIG {
    uuid id PK
    string onlineExamId UK
    string examName
    uuid batchId
    text tags
    int intervalHours
    boolean enabled
    boolean includeDetailedAnswers
    string lastSyncStatus
    text lastSyncError
    int lastSyncSubmissionCount
    timestamp lastSyncedAt
  }

  SYNC_RUN {
    uuid id PK
    uuid syncConfigId FK
    string onlineExamId
    string status
    string triggeredBy
    timestamp startedAt
    timestamp finishedAt
    int studentsFetched
    int submissionsFetched
    int submissionsUpserted
    int detailedAnswersFetched
    int durationMs
    text error
  }

  ONLINE_EXAM {
    string id PK
    string name
    string questionPaperId
    jsonb questionPaper
    timestamp startTime
    timestamp endTime
    int durationMinutes
    text groups
    text centers
    int numSwitches
    jsonb raw
    timestamp lastSyncedAt
  }

  SUBMISSION {
    uuid id PK
    uuid syncConfigId
    string onlineExamId
    string ezExamUserId
    string name
    string email
    string rollNumber
    boolean isAbsent
    decimal totalScore
    decimal maxScore
    decimal percentile
    int rank
    int attemptedCount
    int correctCount
    int incorrectCount
    jsonb sectionBreakdown
    jsonb raw
    timestamp submittedAt
  }

  SUBMISSION_REPORT {
    uuid id PK
    string onlineExamId
    string ezExamUserId
    jsonb report
    timestamp lastSyncedAt
  }

  STUDENT {
    string ezExamUserId PK
    string name
    string email
    string mobile
    string rollNumber
    text groups
    string centre
    int syncedSubmissionCount
    timestamp firstSeenAt
    timestamp lastSeenAt
  }

  BATCH_MRTEST_EXAM {
    uuid id PK
    uuid batchId
    string roadmapItemId
    string mrTestExamId
    string examName
    int sequence
    string prerequisiteCourseId
    smallint prerequisiteThreshold
    string takeUrl
  }

Notable status/enum fields

  • MrTestSyncConfig.lastSyncStatus: idle | running | success | failed | unauthenticated.
  • MrTestSyncRun.status: running | success | failed | unauthenticated.
  • MrTestSyncRun.triggeredBy: scheduled | manual | boot.
  • BatchMrTestExam.prerequisiteThreshold: percent (smallint, default 75 per entity; migration lowered the column default).

Cross-schema note: batch_mrtest_exams lives in the default schema and references mrTestExamId (EzExam) and prerequisiteCourseId (Graphy _id) by id only — no FK across schemas. MrTestSubmission.syncConfigId is a logical reference, not a DB-level FK.


API surface

All routes below mount under /api/ezexam (routes/index.tsthis.router.use('/api/ezexam', this.ezExamRoutes.router)), and every route in EzExamRoutes is guarded by authMiddleware + adminMiddleware (router.use(authMiddleware, adminMiddleware)), so all require an ADMIN/SUPERADMIN JWT. Handlers split across MrTestSyncController (sync), EzExamController (live pass-through), and MrLearnSyncController (shared bulk seed).

Method Path Auth/role Purpose
POST /api/ezexam/auth/login Admin Authenticate the EzExam staff account; store session + encrypted password.
GET /api/ezexam/auth/status Admin Report whether the EzExam session is live.
POST /api/ezexam/auth/logout Admin Clear EzExam session.
GET /api/ezexam/registrable-students Admin List MAS students eligible to push into EzExam.
POST /api/ezexam/students Admin Push a MAS student into EzExam (create student).
GET /api/ezexam/capacity Admin Live EzExam account capacity.
GET /api/ezexam/question-papers Admin Live /staff/get_qpaps.
GET /api/ezexam/online-exams Admin Live /staff/get_onexs.
GET /api/ezexam/online-exams/:onexId/detail Admin Live per-exam bundle (submissions + results + students + content).
POST /api/ezexam/online-exams/:onexId/regenerate-analytics Admin Ask EzExam to regenerate analytics (async upstream).
POST /api/ezexam/proxy Admin Generic authenticated proxy to any /staff/* or /admin/* EzExam path.
GET /api/ezexam/sync/configs Admin List sync configs (newest first).
POST /api/ezexam/sync/configs Admin Create a sync config; schedules repeatable job + an immediate run.
PATCH /api/ezexam/sync/configs/:id Admin Update interval/enabled/detailed/name/batch; reschedules if needed.
DELETE /api/ezexam/sync/configs/:id Admin Delete config (unschedules); ?purgeData=true also deletes submissions + reports.
POST /api/ezexam/sync/configs/:id/run Admin Enqueue an immediate manual sync (202).
GET /api/ezexam/exams-by-tag?tag= Admin Scan the EzExam catalog for exams carrying a student-tag.
POST /api/ezexam/sync/by-tag Admin Bulk-create configs + queue runs for selected tagged exams.
GET /api/ezexam/sync/configs/:id/submissions Admin Paged cached submissions for a config (search by name/email/roll).
GET /api/ezexam/sync/configs/:id/runs Admin Recent sync runs for a config (audit).
GET /api/ezexam/sync/exams Admin List cached online_exams.
GET /api/ezexam/select-fields Admin EzExam filter catalog (classes/groups/centres/cities) for the sidebar.
GET /api/ezexam/students Admin Live paged EzExam students, enriched with hasMasAccount/inMrLearn.
POST /api/ezexam/students/create-mas-account Admin Mint a MAS User (+ optional Application) from an EzExam student row.
POST /api/ezexam/students/seed-all-missing Admin Bulk demo-seed (shared Mr. Learn controller).
GET /api/ezexam/batches Admin Lightweight batch list for the per-row picker.
POST /api/ezexam/sync/batches/:batchId/run Admin Create-if-missing + queue sync for every exam mapped to a batch.
GET /api/ezexam/students/cached Admin Deduped roster from cached submissions (GROUP BY aggregate).
GET /api/ezexam/students/:rollnum/report Admin Live per-student aggregated report from EzExam.
GET /api/ezexam/training/students/:ezExamUserId Admin Assemble per-question AI-training dataset.
GET /api/ezexam/students/:ezExamUserId/analyses Admin Cached parsed analysis blobs, one row per exam.

Related routes outside /api/ezexam

Method Path Auth/role Purpose
PUT /api/admin/mas/batches/:id/mrtest-exams Admin Set the batch → Mr. Test exam map (AdminMasController.setBatchMrTestExams).
GET /api/admin/mas/batches/:id/mrtest-exams Admin Read the batch's mapped exams + per-exam status.
POST /api/student/mrtest-exams/:id/launch Student (auth) Gate + return the EzExam take URL for a mapped exam.

User journeys

Journey 1 — Configure Mr. Test credentials (admin login to EzExam)

Before any sync can run, an admin authenticates the shared EzExam staff account. EzExamService.authenticate performs the two-step Django CSRF dance and persists the session so a server restart does not force re-login. The password is encrypted at rest with an AES-256-GCM key derived from JWT_SECRET.

sequenceDiagram
  participant Admin as Admin UI
  participant API as POST /api/ezexam/auth/login
  participant EZS as EzExamService
  participant EXT as EzExam Django
  participant DB as mrtest.auth_credentials

  Admin->>API: submit username and password
  API->>EZS: authenticate username password
  EZS->>EXT: GET /login
  EXT-->>EZS: csrftoken cookie plus inline csrfmiddlewaretoken
  EZS->>EXT: POST /login with form token and cookie
  alt credentials valid
    EXT-->>EZS: login_status OK plus sessionid cookie
    EZS->>EZS: encrypt password with key from JWT_SECRET
    EZS->>DB: upsert session csrf and encrypted password
    EZS-->>API: AuthState
    API-->>Admin: 200 authenticated
  else login rejected or non-JSON body
    EXT-->>EZS: error or HTML
    EZS-->>API: throw credentials likely invalid
    API-->>Admin: 4xx or 5xx error
  end

Journey 2 — Create a sync config and run the first sync

Creating a config both schedules the repeatable job AND fires a one-off immediate run, so the admin sees data without a second click. The immediate-run enqueue is best-effort — if it blips, the repeatable schedule still covers the next cycle.

sequenceDiagram
  participant Admin as Admin UI
  participant API as POST /api/ezexam/sync/configs
  participant CTL as MrTestSyncController
  participant CFG as sync_configs
  participant QS as QueueService
  participant Q as mrtestSyncQueue

  Admin->>API: onlineExamId examName intervalHours enabled
  API->>CTL: createConfig
  CTL->>CFG: findOne by onlineExamId
  alt config already exists
    CTL-->>API: 409 conflict with existing
  else new
    CTL->>CFG: save config lastSyncStatus idle
    opt enabled
      CTL->>QS: scheduleMrTestSync configId intervalHours
      QS->>Q: add repeatable job stable jobId
      CTL->>QS: triggerMrTestSyncNow configId
      QS->>Q: add one-off job triggeredBy manual
    end
    CTL-->>API: 201 created
  end

Journey 3 — Scheduled per-exam sync (the core pipeline)

This is the heart of the integration. The worker pulls the config, calls runSyncForConfig, which refreshes the exam snapshot, pulls result/submission rows, backfills emails from the roster CSV, upserts submissions + the deduped student roster, optionally pulls detailed answers + parsed analysis, credits XP for first-seen submissions, and records a SyncRun.

sequenceDiagram
  participant Q as mrtestSyncQueue
  participant W as mrtestSync.worker
  participant SYS as MrTestSyncService
  participant EXT as EzExam
  participant DB as mrtest tables
  participant XP as StudentProgress and Badge
  participant N as NotificationService

  Q->>W: job with configId and triggeredBy
  W->>SYS: runSyncForConfig configId triggeredBy
  SYS->>DB: insert SyncRun status running
  SYS->>DB: set config lastSyncStatus running
  SYS->>EXT: get_onexs and get_qpaps
  alt onex missing upstream
    SYS->>DB: SyncRun failed exam not found
    SYS-->>W: return failed run
  else onex found
    SYS->>DB: upsert online_exams snapshot
    SYS->>EXT: get_res for leaderboard rows
    alt get_res empty
      SYS->>EXT: get_subm fallback
    end
    SYS->>EXT: get_studs_csv to map roll to email
    loop each submission row
      SYS->>SYS: mapSubmissionRow and backfill email by roll
      SYS->>DB: upsert submission keyed on exam and user
      alt brand new submission and not absent
        SYS->>XP: grantXp test_taken skipStreakUpdate
        SYS->>N: notify mrtest_new_score when score present
      end
      SYS->>DB: upsert deduped student row
    end
    opt includeDetailedAnswers
      SYS->>EXT: get_subjective and get_omr_resps
      loop each row with rollnum
        SYS->>EXT: GET analysis HTML page
        SYS->>SYS: parseStudentAnalysisHtml to structured blob
        SYS->>DB: upsert submission_report
      end
    end
    SYS->>DB: SyncRun success with counts and durationMs
    SYS->>DB: config lastSyncStatus success and lastSyncedAt
    SYS-->>W: return success run
  end

Journey 4 — Sync by batch and by tag

Two bulk entry points share the same idempotent shape: ensure a config exists per exam (creating + scheduling when missing), then queue an immediate run for each.

sequenceDiagram
  participant Admin as Admin UI
  participant API as sync batch or sync by-tag
  participant CTL as MrTestSyncController
  participant DB as sync_configs and batch_mrtest_exams
  participant EXT as EzExam catalog
  participant QS as QueueService

  alt sync by batch
    Admin->>API: POST sync/batches/:batchId/run
    CTL->>DB: load BatchMrTestExam rows for batch
    CTL->>CTL: dedupe exam ids and resolve names
  else sync by tag
    Admin->>API: GET exams-by-tag then POST sync/by-tag
    CTL->>EXT: page qexams and filter by swith tag
    CTL->>Admin: list of matching exams
    Admin->>API: POST sync/by-tag with selected exams
  end
  loop each exam id
    CTL->>DB: find config by onlineExamId
    alt config missing
      CTL->>DB: create config interval 24 enabled
      CTL->>QS: scheduleMrTestSync
    else exists
      CTL->>DB: backfill batchId or refresh tags
    end
    CTL->>QS: triggerMrTestSyncNow
  end
  CTL-->>Admin: 202 queued N of M

Journey 5 — Parse a per-student analysis report

When includeDetailedAnswers is on, the sync fetches EzExam's server-rendered analysis page (no JSON equivalent exists) and runs the regex-based parser to extract overall scores, per-subject sections, per-difficulty accuracy, topic accuracy, the subject-comparison chart, and pre-computed diagnostics. Only the parsed blob is stored; the raw HTML is discarded.

sequenceDiagram
  participant SYS as MrTestSyncService
  participant EXT as EzExam
  participant P as parseStudentAnalysisHtml
  participant DB as submission_reports

  SYS->>EXT: GET students rollnum analysis qpapId
  EXT-->>SYS: server rendered HTML page
  SYS->>P: parse HTML
  P->>P: strip tags to plain text for top line score and time
  P->>P: read main scoreboard table per subject and total
  P->>P: read per subject cards for max marks time and rank
  P->>P: read acc-diff-rsp tables for easy medium hard
  P->>P: read topic-perf tables for per topic accuracy
  P->>P: read inline subjCompConfig for chart datasets
  P->>P: compute diagnostics strongest weakest weak topics
  alt parse succeeds
    P-->>SYS: StudentAnalysis structured blob
    SYS->>DB: store report.analysis only
  else parse throws
    P-->>SYS: error logged non-blocking
    SYS->>DB: store report without analysis
  end

The parser is regex-based on purpose (avoids pulling cheerio for one page). It tolerates small markup drift but will break on a full EzExam redesign. StudentAnalysis.htmlBytes echoes the input length so callers can detect a login-redirect/404 stub instead of a real page. Cached blobs are served read-only by GET /api/ezexam/students/:ezExamUserId/analyses.

Journey 6 — Build AI-training data from submissions

On-demand and expensive (3-4 EzExam calls per exam), so it is deliberately kept out of the sync cycle. It joins each cached submission with live question + answer-key + response payloads to emit one flat record per question. It has three modes — cached, live, empty.

sequenceDiagram
  participant Admin as Admin UI
  participant API as GET training/students/:ezExamUserId
  participant TDS as MrTestTrainingDataService
  participant DB as mrtest tables
  participant EXT as EzExam

  Admin->>API: request training data optional onlineExamId
  API->>TDS: getStudentTrainingData
  TDS->>DB: find cached submissions for student
  alt cached submissions found
    Note over TDS: mode cached
  else none cached
    TDS->>DB: load enabled sync configs
    TDS->>EXT: top up with recent qexams when under 30
    TDS->>EXT: discoverLive scan get_subm concurrency 5
    Note over TDS: mode live or empty
  end
  loop each submission
    TDS->>EXT: get exam content omr subjective and submission log
    TDS->>TDS: index responses by qno and join per question
  end
  TDS-->>API: records examCount questionCount mode
  API-->>Admin: 200 dataset

Journey 7 — Map an exam to a batch roadmap, then student launch

Admins attach an EzExam exam to a roadmap item with an optional Mr. Learn prerequisite. When a student launches, BatchService.canStudentAttemptTest gates on the prerequisite course progress before handing back the take URL.

sequenceDiagram
  participant Admin as Admin UI
  participant AAPI as PUT batches/:id/mrtest-exams
  participant BSV as BatchService
  participant BME as batch_mrtest_exams
  participant Student as Student dashboard
  participant SAPI as POST mrtest-exams/:id/launch
  participant STC as StudentController

  Admin->>AAPI: assignments roadmapItem exams prereq threshold
  AAPI->>BSV: setBatchMrTestExams
  BSV->>BSV: validate roadmap ids and prereq course attached
  BSV->>BME: replace rows for batch
  AAPI-->>Admin: 200 saved

  Student->>SAPI: launch mapped exam row id
  SAPI->>STC: launchMrTestExam
  STC->>STC: verify enrolled in batch unless staff
  STC->>BSV: canStudentAttemptTest userId rowId
  alt no prerequisite course
    BSV-->>STC: unlocked true
  else prerequisite set
    BSV->>BSV: match Mr Learn learner progress vs threshold
    alt progress below threshold
      BSV-->>STC: unlocked false reason course-not-started
      STC-->>Student: 403 TEST_LOCKED with blocker payload
    else met or completed
      BSV-->>STC: unlocked true
    end
  end
  opt unlocked
    STC->>STC: resolve takeUrl stored then catalog then constructed
    STC-->>Student: 200 takeUrl examName
  end

Journey 8 — Mint a MAS account from an EzExam student row

EzExam student rows lack an email, so the admin supplies one. The handler is idempotent: re-running on the same email backfills missing fields, optionally rotates the password, and brings onboarding flags up to enrolled.

sequenceDiagram
  participant Admin as Admin UI
  participant API as POST students/create-mas-account
  participant CTL as MrTestSyncController
  participant DB as users and applications

  Admin->>API: email fullName phone rollnum batchId
  API->>CTL: createMasAccount
  CTL->>CTL: validate email and fullName and password length
  CTL->>DB: find user by lower email
  alt user missing
    CTL->>DB: create user verified profileComplete stage 3
  else user exists
    CTL->>DB: backfill name phone and bump verification
    opt password supplied
      CTL->>DB: rehash password
    end
  end
  opt batchId supplied
    CTL->>DB: create or patch Application status ENROLLED all flags true
  end
  opt seedDemo true
    CTL->>DB: seedDemoData for dashboard
  end
  CTL-->>Admin: 200 userId applicationCreated

Background jobs & async

  • Queue: mrtestSyncQueue (BullMQ), created in QueueService with removeOnComplete: 50 / removeOnFail: 50.
  • Worker: src/workers/mrtestSync.worker.ts — listens on mrtestSyncQueue, concurrency: 1 (serial, because the EzExam session shares one rolling CSRF token). Initializes its own DB connection on import. Started in src/index.ts (await import('./workers/mrtestSync.worker')).
  • Job payload: { type: 'mrtestSync', configId, triggeredBy } where triggeredBy ∈ { scheduled, manual, boot }.
  • Scheduling (QueueService):
  • scheduleMrTestSync(configId, intervalHours) — adds a repeatable job with a stable jobId mrtest-sync-<configId> and repeat.every = intervalHours * 3600_000 ms. Calls unscheduleMrTestSync first so updates replace the old schedule. Throws if intervalHours < 1.
  • unscheduleMrTestSync(configId) — removes the repeatable job by matching key.
  • triggerMrTestSyncNow(configId) — adds a one-off job (triggeredBy: 'manual').
  • Boot re-scheduling: src/index.ts loads all enabled MrTestSyncConfig rows on boot and re-calls scheduleMrTestSync for each, so config changes always reach BullMQ even after a Redis flush.
  • Worker skip rules: if config not found → returns { skipped: true, reason: 'config-not-found' }; if config disabled and triggeredBy === 'scheduled' → returns { skipped: true, reason: 'config-disabled' } (a manual trigger still runs even when disabled).
  • Side-effects on first-seen submission: XP grant via StudentProgressService.grantXp(userId, 'test_taken', submissionId, { skipStreakUpdate: true }), badge re-evaluation via BadgeService.evaluateForUser, and a mrtest_new_score notification via NotificationService.fromTemplate. All are non-blocking (errors logged, never re-thrown).
  • No socket events / webhooks: this integration is pull-only; EzExam does not push to MAS.

External integrations

EzExam (Mr. Test) — EzExamService (src/services/EzExamService.ts)

  • Base URL: EZEXAM_BASE_URL (default https://myanalyticsschool.ezexam.in).
  • Auth scheme: Django form login. GET /login harvests the csrftoken cookie + an inline csrfmiddlewaretoken; POST /login returns a sessionid cookie (and rotates csrftoken). All /staff/* calls send both cookies; mutating calls also send the rolling csrfmiddlewaretoken in the body. Session is cached in-memory and persisted to mrtest.auth_credentials so restarts don't force re-login.
  • Password at rest: AES-256-GCM with a key derived from JWT_SECRET (sha256(JWT_SECRET)). Rotating JWT_SECRET invalidates the stored password — admin must re-login.
  • Key upstream endpoints used: /staff/get_onexs, /staff/get_qpaps, /staff/get_qexams (paged catalog), /staff/get_res, /staff/get_subm, /staff/get_studs_csv, /staff/get_subjective, /staff/get_omr_resps, /staff/studs_select_fields + /staff/get_studs_paging, /staff/get_submission_log, /staff/get_onex_content; plus server-rendered GET /students/<rollnum>/analysis/<qpapId> (HTML) and GET /students/<rollnum>/report.

Failure / fallback behavior

  • Result-source fallback: prefer /staff/get_res (leaderboard rows). If empty, fall back to /staff/get_subm, then to "largest array in the response object" heuristic, because EzExam keys submissions differently per exam type (subm/submissions/studs/results/attempts/...).
  • Email backfill: result/submission rows often omit email; the sync pulls the roster CSV to build a roll → email map and backfills. Non-blocking on failure.
  • Auth-error classification: when runSyncForConfig catches an error matching /not authenticated|401|403/i, the run + config are marked unauthenticated (distinct from failed) so the UI can prompt a re-login.
  • Detailed-answers + analysis: each sub-step (get_subjective, get_omr_resps, analysis fetch, analysis parse) is independently try/caught and non-blocking — a parse failure stores the report without analysis.
  • Training-data resilience: per-exam fan-out calls use .catch(() => null); live discovery runs concurrency 5 to balance latency vs EzExam 429 rate-limiting.
  • Take-URL resolution (student launch): stored takeUrl → live catalog surl lookup → constructed ${EZEXAM_BASE_URL}/s/exams/<id>/take.

Env vars: EZEXAM_BASE_URL, JWT_SECRET (encryption key), REDIS_HOST/REDIS_PORT (worker + queue). No explicit feature flag — the integration self-disables in practice when no credentials are authenticated (sync runs fail with unauthenticated).


Status lifecycles

MrTestSyncRun.status (audit record per execution)

stateDiagram-v2
  [*] --> running : run created
  running --> success : exam refreshed and rows upserted
  running --> failed : onex missing or non-auth error
  running --> unauthenticated : error matches 401 403 or not authenticated
  success --> [*]
  failed --> [*]
  unauthenticated --> [*]

MrTestSyncConfig.lastSyncStatus (live mirror of the latest run, shown in the admin UI)

stateDiagram-v2
  [*] --> idle : config created
  idle --> running : sync starts
  running --> success : sync completes
  running --> failed : sync error
  running --> unauthenticated : EzExam session expired
  success --> running : next scheduled or manual run
  failed --> running : retry
  unauthenticated --> running : after re-login and retry

Student test unlock (gate) — derived from BatchService.canStudentAttemptTest

stateDiagram-v2
  [*] --> evaluating : student launches
  evaluating --> unlocked : no prerequisite course
  evaluating --> checkingProgress : prerequisite set
  checkingProgress --> unlocked : progress at or above threshold or completed
  checkingProgress --> locked : below threshold or course not started
  locked --> [*] : 403 TEST_LOCKED
  unlocked --> [*] : 200 takeUrl returned

Edge cases, limits & gotchas

  • Serial worker by design: concurrency: 1. EzExam's rolling csrfmiddlewaretoken means parallel sessions race; do not raise concurrency.
  • One config per exam: sync_configs.onlineExamId is unique. createConfig returns 409 if a config already exists.
  • Idempotent upserts: submissions keyed on (onlineExamId, ezExamUserId); reports on the same pair; students on ezExamUserId. upsertStudent never overwrites a populated field with an empty value (partial data from one exam can't blank fields set by another).
  • XP credited only on first sight: upsertSubmission returns true only for brand-new rows; XP/notification fire only then. Re-syncs do not re-credit. XP grant uses skipStreakUpdate: true — PRD decision so a back-dated dump can't retroactively bump a streak. Absent submissions are skipped.
  • Email matching nuance: EzExam emails may carry a Graphy-style org prefix; creditTestTaken matches User.email against both raw and de-prefixed variants (emailVariantsForUserLookup).
  • ezExamUserId fallback chain: numeric id → roll → email. Rows with no resolvable id are skipped with a warning. This means the "stable" key can shift if EzExam later exposes a real id for a previously roll-keyed student.
  • Dynamic result columns: /staff/get_res returns section/total columns whose header carries the max-score in parentheses (e.g. "Total (180)"); mapSubmissionRow discovers them by regex rather than fixed keys.
  • EzExam filter trap: listStudentsLive must populate every filter slot with all catalog ids (empty array = "no results"), and must route a search term to either name or rollnum (EzExam ANDs non-empty slots, so filling both returns 0 rows).
  • Tag scanning is client-side: EzExam has no server-side tag filter, so exams-by-tag pages the whole catalog (capped at 80 pages as a safety valve).
  • Detailed answers are expensive: enabling includeDetailedAnswers adds an analysis-page fetch per submission. Training-data assembly fans out 3-4 calls per exam — clients are expected to cache.
  • Delete is non-destructive by default: DELETE /sync/configs/:id only unschedules + deletes the config unless ?purgeData=true (then also deletes submissions + reports). Cached online_exams and students rows are not purged.
  • Cross-schema, no FKs: mrtest.* and batch_mrtest_exams (default schema) reference EzExam/Graphy ids by value only. TypeORM auto-sync is on; the mrtest schema must exist.
  • No multi-tenant x-platform branching here: the integration targets the single MAS EzExam staff account; the x-platform header used elsewhere in the suite does not alter this domain.
  • Analysis parser fragility: regex-based; tolerant of small drift, breaks on a full redesign. Check htmlBytes to confirm a real page was scraped (not a login redirect/404 stub).
  • prerequisiteThreshold default mismatch: entity declares default 75; a migration (1762600000000-LowerMrTestPrereqThresholdDefault) lowered the column default — verify the live default before relying on it.