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) andsurl(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_examslives in the default schema and referencesmrTestExamId(EzExam) andprerequisiteCourseId(Graphy_id) by id only — no FK across schemas.MrTestSubmission.syncConfigIdis a logical reference, not a DB-level FK.
API surface¶
All routes below mount under /api/ezexam (routes/index.ts → this.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.htmlBytesechoes the input length so callers can detect a login-redirect/404 stub instead of a real page. Cached blobs are served read-only byGET /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 inQueueServicewithremoveOnComplete: 50/removeOnFail: 50. - Worker:
src/workers/mrtestSync.worker.ts— listens onmrtestSyncQueue,concurrency: 1(serial, because the EzExam session shares one rolling CSRF token). Initializes its own DB connection on import. Started insrc/index.ts(await import('./workers/mrtestSync.worker')). - Job payload:
{ type: 'mrtestSync', configId, triggeredBy }wheretriggeredBy ∈ { scheduled, manual, boot }. - Scheduling (
QueueService): scheduleMrTestSync(configId, intervalHours)— adds a repeatable job with a stablejobIdmrtest-sync-<configId>andrepeat.every = intervalHours * 3600_000ms. CallsunscheduleMrTestSyncfirst so updates replace the old schedule. Throws ifintervalHours < 1.unscheduleMrTestSync(configId)— removes the repeatable job by matching key.triggerMrTestSyncNow(configId)— adds a one-off job (triggeredBy: 'manual').- Boot re-scheduling:
src/index.tsloads all enabledMrTestSyncConfigrows on boot and re-callsscheduleMrTestSyncfor 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 andtriggeredBy === '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 viaBadgeService.evaluateForUser, and amrtest_new_scorenotification viaNotificationService.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(defaulthttps://myanalyticsschool.ezexam.in). - Auth scheme: Django form login.
GET /loginharvests thecsrftokencookie + an inlinecsrfmiddlewaretoken;POST /loginreturns asessionidcookie (and rotatescsrftoken). All/staff/*calls send both cookies; mutating calls also send the rollingcsrfmiddlewaretokenin the body. Session is cached in-memory and persisted tomrtest.auth_credentialsso restarts don't force re-login. - Password at rest: AES-256-GCM with a key derived from
JWT_SECRET(sha256(JWT_SECRET)). RotatingJWT_SECRETinvalidates 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-renderedGET /students/<rollnum>/analysis/<qpapId>(HTML) andGET /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 → emailmap and backfills. Non-blocking on failure. - Auth-error classification: when
runSyncForConfigcatches an error matching/not authenticated|401|403/i, the run + config are markedunauthenticated(distinct fromfailed) 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 withoutanalysis. - 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 catalogsurllookup → 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 rollingcsrfmiddlewaretokenmeans parallel sessions race; do not raise concurrency. - One config per exam:
sync_configs.onlineExamIdis unique.createConfigreturns 409 if a config already exists. - Idempotent upserts: submissions keyed on
(onlineExamId, ezExamUserId); reports on the same pair; students onezExamUserId.upsertStudentnever 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:
upsertSubmissionreturnstrueonly for brand-new rows; XP/notification fire only then. Re-syncs do not re-credit. XP grant usesskipStreakUpdate: 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;
creditTestTakenmatchesUser.emailagainst both raw and de-prefixed variants (emailVariantsForUserLookup). ezExamUserIdfallback 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_resreturns section/total columns whose header carries the max-score in parentheses (e.g."Total (180)");mapSubmissionRowdiscovers them by regex rather than fixed keys. - EzExam filter trap:
listStudentsLivemust 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-tagpages the whole catalog (capped at 80 pages as a safety valve). - Detailed answers are expensive: enabling
includeDetailedAnswersadds 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/:idonly unschedules + deletes the config unless?purgeData=true(then also deletes submissions + reports). Cachedonline_examsandstudentsrows are not purged. - Cross-schema, no FKs:
mrtest.*andbatch_mrtest_exams(default schema) reference EzExam/Graphy ids by value only. TypeORM auto-sync is on; themrtestschema must exist. - No multi-tenant
x-platformbranching here: the integration targets the single MAS EzExam staff account; thex-platformheader 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
htmlBytesto confirm a real page was scraped (not a login redirect/404 stub). prerequisiteThresholddefault mismatch: entity declares default 75; a migration (1762600000000-LowerMrTestPrereqThresholdDefault) lowered the column default — verify the live default before relying on it.
Related docs¶
- Assessments — Quizzes, Assignments — internal assessment surfaces (contrast with external Mr. Test exams).
- Education — LMS Courses & Batches — batches, roadmap items, and the course model that
BatchMrTestExammaps into. - Integration — Mr. Learn (External LMS Sync) — the sibling Graphy LMS sync; supplies the prerequisite-course progress that gates Mr. Test launches.
- Student Engagement & Gamification — the XP/badge/notification system credited on first-seen submissions.
- Student Portal & Profile — the student dashboard that launches mapped Mr. Test exams.
- Identity & Access —
User/Applicationmodel and roles used by account minting and launch authorization.