Task #193
Updated by Ayush Yadav on 09/21/2026 12:55 PM
h1. Harshit's task list — Gantt W11 to W14 (21 Sep to 16 Oct 2026) h2. Read this first: your lane has changed Until now you worked on the React frontend. From *21 September 2026 you own the Spring Boot backend and the admin panel*. Nikita has taken over the student-facing React work on issue #194. Everything you built in W9 and W10 stays — the Plans page, the FREE / PREMIUM badges, the payment page. Do not delete any of it. Nikita will build on top of it. Tickets you will comment on: #95 (M4), #96 (M5), #97 (M6), #98 (M7), #99 (M8), Harshit owns what students see. Redmine tickets he touches: #100 (M9), #101 (M10), #102 (M11), #103 (M12), #93 (M2), #94 (M3), #112 (QB-1). *Do not create new tickets.* then #99 (M8). h2. How to read every task below Each task always has the same four parts: * *Goal* — one sentence saying what the finished thing does. * *Why* — the reason it matters, so you understand the task instead of just copying it. * *Steps* — what to actually change, with the real file paths from our repository. * *Done when* — how you prove to yourself it works before you say it is finished. h2. The calendar (no Saturdays, no Sundays) |_. Week |_. Working days | | W11 | Mon 21, Tue 22, Wed 23, Thu 24, Fri 25 September | | W12 | Mon 28, Tue 29, Wed 30 September, Thu 1, Fri 2 October — Phase 1 MVP must be finished by this Friday | | W13 | Mon 5, Tue 6, Wed 7, Thu 8, Fri 9 October (W9) — Phase 2 opens, JEE Main bank track opens | | W14 | Mon 12, Tue 13, Wed 14, Thu 15, Fri 16 October | That is 20 working days membership page and 20 tasks. h2. Ground rules for all your work * Backend runs on @http://localhost:8081/api@. Frontend runs on @http://localhost:5173@. Never use port 8080. * In a controller write <code>@RequestMapping("/tests")</code>, *not* <code>@RequestMapping("/api/tests")</code>. The @/api@ part is added automatically by the context path. * Constructor injection everywhere. Validate request bodies with <code>@Valid</code> DTOs. Return errors as @{"error": "..."}@ through @GlobalExceptionHandler@. * Never commit a @.env@ file, a password, an API key, or a webhook URL. * There are only *two* question stores and they must never be mixed: the shared catalog at @/admin/questions@, and the six per-exam banks at @/exam-questions@. tags h2. Already built Task H1 — do not rebuild these A full code audit on 21 September found that several things our old notes call "pending" are actually finished. Skip them: * Bonus marks, negative marks, partial marking for multiple-correct, and numerical tolerance are all live in @AssessmentService.gradeObjective@. * The @AttemptAuditLog@ entity and @GET /attempts/{id}/audit@ both exist. * Admin already has Faculty vs Candidate tabs and a subscription-plan dropdown. * @PlanRules.isPremium@ exists on the backend. * Bulk candidate upload by CSV and Excel works. h2. The biggest problem we are fixing, and why W11 looks like it does Our backend has a complete, working exam engine. It starts attempts, saves answers, submits, scores Plans page with negative and bonus marks, and calculates rank and percentile. *But no student screen calls it.* The only code that touches @/attempts@ is an admin testing harness in @TestAuthoringPanel.tsx@. Nikita connects the student screens this week. Your job in W11 is to make that engine trustworthy: enforce the clock, enforce the navigation rules, and stop giving away premium accounts for free. h1. Week 11 — Mon 21 to Fri 25 September Theme: make the exam engine trustworthy, and close the two Gantt modules that end this week — M5 auto evaluation and M7 institution management. h3. H1 — Monday 21 September: Make Razorpay payment verification actually verify something (M9, boxes (M9 extra + #100) *Goal.* When someone finishes Build a payment, we confirm the payment really came from Razorpay before we give them a premium plan. *Why.* Right now @PaymentService.verify@ marks the order as PAID and upgrades the user to MONTHLY *without checking anything new page at all*. Anyone who knows the URL can call that endpoint and get a free premium account. /pricing with two side-by-side containers: Free vs Premium. This is a security hole, so it goes first. the “categorization info” page. *Steps.* # From scratch: Open @backend/src/main/java/com/teamtalks/backend/services/PaymentService.java@. # Razorpay sends back three values: @razorpay_order_id@, @razorpay_payment_id@ [App.tsx](Frontend/src/App.tsx). See how /rank-predictor is registered. Copy that pattern. Create [Frontend/src/pages/Membership.tsx](Frontend/src/pages/Membership.tsx) and @razorpay_signature@. Join Membership.css next to it. Use the first two with a @|@ between them, for example @order_abc|pay_xyz@. # Run HMAC-SHA256 over that joined string using your Razorpay key secret same page shell as the key. Java has this built in: @Mac.getInstance("HmacSHA256")@. Convert the result to lowercase hexadecimal. # Compare your hex string to the signature Razorpay sent. Compare the *bytes* using @MessageDigest.isEqual@, not @String.equals@ — that avoids Courses (courses-page in [Assesment.css](Frontend/src/pages/Assesment.css)). Two columns. Left box title Free. Right box title Premium. In each box, list benefits in simple bullets: Free: sample PYQs, Practice Hub, Rank Predictor, a timing attack. # few mocks Premium: full PYQs, all mocks, all exam tracks, AI-generated questions If they do not match, throw an error so the client gets @{"error": "Payment verification failed"}@. Do not mark the order paid. # Stop hardcoding MONTHLY. Store which plan the user was buying on the @PaymentOrder@ row when the order is created, and set *that* plan on success. # Keep the stub behaviour for local development: if @RAZORPAY_KEY_ID@ is not set, skip the signature check but return @"stub": true@ so logged in, read getLoggedInUser()?.subscriptionPlan. If it is obvious we are not in real mode. *Done when.* Calling @POST /api/payments/verify@ with a made-up signature returns an error and FREEMIUM (or empty), mark the user's plan does not change. With Free box Your plan. Otherwise mark the correct signature the order becomes PAID and the Premium box Your plan becomes whatever was ordered. Never print the key secret in a log line. h3. H2 — Tuesday 22 September: Stop students submitting after time is up (M4, #95) *Goal.* The server refuses to save or submit an attempt once the allowed time has passed, and closes off abandoned attempts by itself. *Why.* Today show the countdown clock only exists in the browser. A student can pause the JavaScript, reopen the page hours later, or call the API directly, and we will happily accept the answers. The clock must be enforced on the server, because the server is the only thing a student cannot edit. *Steps.* # Open @backend/src/main/java/com/teamtalks/backend/services/AssessmentService.java@. # Write one small private helper, for example @assertNotExpired(AssessmentAttempt attempt)@. It takes @startedAt@, adds the test's @durationMinutes@, and compares that deadline real name (MONTHLY, etc.). Free button: go to the current time. # Call it at the top of @save@ and at the top of @submit@. # When the deadline has passed do not simply reject the call. Mark the attempt submitted, run the normal @evaluate@ scoring on whatever answers were already saved, write an audit row with action @AUTO_SUBMIT@, and then return a clear error saying the time expired. # Allow about 30 seconds of grace, so a student whose answer is in flight when the clock hits zero does not lose it. # Also check the test's own @startAt@ and @endAt@ window. An attempt cannot begin before the test opens or after it closes. There is already an @assertWindow@ method — reuse it, do not write a second one. *Done when.* Create a test with a 1 minute duration, start it, wait two minutes, then try /practicehome. Premium button: go to save. You get an error, the attempt shows SUBMITTED with a score, and @GET /api/attempts/{id}/audit@ shows the @AUTO_SUBMIT@ row. h3. H3 — Wednesday 23 September: Enforce navigation modes and record tab switches (M4, #95) *Goal.* The three navigation modes we already store actually restrict what a student can do, and switching browser tabs during an exam leaves a permanent record. *Why.* @AssessmentTest@ has a @navMode@ column holding FREE, SEQUENTIAL or SECTION_LOCK. We send it to the browser and then ignore it completely. A real exam engine has to stop a student jumping back into a section they already closed. Separately, the audit code has a comment saying it supports a @TAB_SWITCH@ action, but *nothing in the codebase ever writes one*, so that feature does not really exist yet. *Steps.* # Still in @AssessmentService.java@, inside @save@, look at the test's @navMode@ before accepting an answer. # *SEQUENTIAL*: only accept an answer /pricing for the current question now, or one already reached. Track the furthest question index reached on the attempt row so you have something to compare against. # *SECTION_LOCK*: once a student moves from section 1 short “Ask admin to section 2, refuse any further answers for section 1. Add a small @completedSectionIds@ field on @AssessmentAttempt@ and update it when the section changes. # *FREE*: accept everything, which is what happens today. # When a rule is broken return a clear message such as @{"error": "This section is already closed"}@. upgrade” note. Do not silently ignore the answer. # The save request body already carries @tabSwitchCount@. When that number is higher than what we stored, call @auditService.log(attemptId, userId, "TAB_SWITCH", ...)@ once per new switch, then update the stored count. *Done when.* On a SECTION_LOCK test, moving to section 2 and then answering a section 1 question returns an error. Switching tabs three times produces three @TAB_SWITCH@ rows wire live payment. Add { label: "Plans", href: "/pricing" } in the audit endpoint. h3. H4 — Thursday 24 September: Range-based numerical answers (M5, #96) — this closes M5 *Goal.* A numerical question can accept any answer inside a range, not only one exact number. *Why.* JEE [Header.tsx](Frontend/src/components/Header.tsx) after Practice. Check desktop and NEET regularly publish answers as a band, for example "any value between 9.78 and 9.82". Today @TestQuestion@ only has a single @tolerance@ field, which means plus-or-minus the same amount on narrow window. Click Plans, read both sides of one number. That cannot express a band that is not symmetric. M5 ends boxes, sign in W11, so this is the last chance. *Steps.* # Open @backend/src/main/java/com/teamtalks/backend/entity/TestQuestion.java@ as Free and add two nullable @Double@ columns: @answerMin@ and @answerMax@. # Add the same two fields to the request DTO used when creating or updating a test, with validation: if one confirm Your plan is given the other must be too, and @answerMin@ must not be greater than @answerMax@. # In @gradeObjective@, in the NUMERICAL branch: when both are set, mark the answer correct if the student's number falls inside the band, including both ends. Otherwise fall back to the existing exact-value-plus-tolerance logic. *Do not delete the old logic* — existing questions depend on it. # Parse the student's typed answer safely. If they typed letters, treat it as wrong rather than letting a @NumberFormatException@ crash the request. Free. *Done when.* A question with @answerMin@ 9.78 and @answerMax@ 9.82 marks 9.8 correct and 9.9 wrong. An old question that only has @tolerance@ still scores exactly as it did before. h3. H5 Task H2 — Friday 25 September: Institution, Department, Class and Batch (M7, #98) — this closes M7 *Goal.* A real Institution record in the database, with departments, classes and batches underneath it, so a coaching centre can organise its students. *Why.* Today "institution" is just a text field copied onto every user, plus an @institutionCode@ string Put FREE / PREMIUM stickers on tests. There is *no* Institution table. That means we cannot list a school's students reliably, cannot group them into batches, and cannot produce per-batch reports. The Gantt asks for department, class and batch management and this is the last week for M7. *Steps.* # Create @entity/Institution.java@ with @id@, @code@ (unique), @name@, @address@, @contactEmail@, @active@, @createdAt@. The @code@ must match the @institutionCode@ strings already stored on users and tests, so existing data keeps working. # Create @entity/Batch.java@ with @id@, a <code>@ManyToOne</code> link exam cards (M9 extra) Open [streamExams.ts](Frontend/src/course/streamExams.ts). Add segment: "FREE" | "PREMIUM" to Institution, @name@, @department@, @classLevel@, @academicYear@, @active@. # Add a nullable <code>@ManyToOne</code> @batch@ field on @Users@. Leave the existing text fields alone so nothing breaks. # Create repositories for both, in type. Tag about half the same style as the other repositories under @repository/@. # Create @InstitutionService@ exams FREE (JEE Main, NEET, CAT can be FREE samples) and add the endpoints to the *existing* @AdminController@, not a new controller: @GET /admin/institutions@, @POST /admin/institutions@, @GET /admin/institutions/{id}/batches@, @POST /admin/institutions/{id}/batches@, @PUT /admin/users/{userId}/batch@. # rest PREMIUM. Write a small one-time migration that reads every distinct non-empty institution name already on @Users@ and creates an Institution row for it, so we do not lose the data we already have. # Restrict these endpoints to Super Admin and Institution Admin, and make sure an Institution Admin can only ever see their own institution. *Done when.* You can create an institution, create two batches under it, move a student into a batch, and list only that batch's students. Logged in as Institution Admin for institution A, you cannot see institution B. h1. Week 12 — Mon 28 September to Fri 2 October Theme: finish Phase 1. Everything same tag on the Gantt for M6, M8, M9 and M10 ends this Friday. h3. H6 — Monday 28 September: Let students upload answer files (M6, #97) *Goal.* A student answering a descriptive question can attach a PDF, a Word file, or a photo of their handwritten answer. *Why.* Descriptive questions today only accept typed text matching rows in @AttemptAnswer.responseText@. For long answers, essays and case studies, students need to attach work. This is the first of three descriptive tasks and the other two depend on it. *Steps.* # Create @entity/AnswerAttachment.java@: @id@, link to @AttemptAnswer@, @fileName@, @contentType@, @sizeBytes@, @storagePath@, @uploadedAt@. # [courses.ts](Frontend/src/course/courses.ts). Open [ExamTicketCard.tsx](Frontend/src/components/ExamTicketCard.tsx). Add @POST /attempts/{attemptId}/answers/{testQuestionId}/attachment@ on @AttemptController@, taking a @MultipartFile@. # Validate hard *before* saving anything. Allow only PDF, DOCX, PNG and JPEG. Cap the size at 10 MB. Check the real content type, not just the file extension — an extension is trivially faked. # *Never* store the file under the name the student gave it. Generate optional segment prop. Show a UUID filename. A name like @../../application.properties@ would otherwise let someone write outside our folder. # Save files under a configurable folder. Add @app.upload.dir@ small badge Free or Premium next to @application.properties@ with a sensible default, and create the folder at startup if it is missing. # Add a download endpoint. Only the student who owns the attempt, the test's author, and admins may download. # Reject uploads once the attempt is submitted. *Done when.* Upload a 2 MB PDF against a descriptive question and download it back unchanged. A 20 MB file is rejected. A @.exe@ renamed to @.pdf@ is rejected. A different student requesting the file gets an error. h3. H7 — Tuesday 29 September: Rubric-based marking (M6, #97) *Goal.* An examiner marks a long answer against a list of named criteria instead of guessing one overall number. *Why.* "Give this essay a mark out of 20" produces wildly different marks exam name. Pass segment from different examiners. A rubric — structure 5, content 8, grammar 4, examples 3 — makes marking consistent Practice Hub and lets us explain the mark to the student. *Steps.* # Create @entity/Rubric.java@ (@id@, @name@, @description@, @createdBy@) and @entity/RubricCriterion.java@ (@id@, link to rubric, @name@, @description@, @maxMarks@, @displayOrder@). # Add a nullable link from @TestQuestion@ to a Rubric, so a descriptive question can carry one. # Create @entity/RubricScore.java@: link to @AttemptAnswer@, link to @RubricCriterion@, @marksAwarded@, @comment@, @evaluatorId@. # Add admin endpoints @GET /admin/rubrics@, @POST /admin/rubrics@, @PUT /admin/rubrics/{id}@. # Extend the existing @PUT /attempts/{id}/manual-score@ so it can accept a list of per-criterion marks. Add the criteria up to get the total and write that into the answer's @marksAwarded@. # Validate that no criterion mark is negative or above its @maxMarks@, and that the criteria actually belong to the rubric attached to that question. # After saving, call @evaluate@ again so the attempt total, rank and percentile all refresh. *Done when.* Create a rubric with four criteria totalling 20, attach it to a descriptive question, mark a student's answer criterion by criterion, and confirm the attempt total went up by exactly the sum you entered. h3. H8 — Wednesday 30 September: Two independent examiners and a moderator (M6, #97) — this closes M6 *Goal.* Two examiners mark the same answer without seeing each other's marks, and if Courses when they disagree too much a senior reviewer decides render the final mark. card. *Why.* This is standard practice for high-stakes descriptive papers and it is the last piece of M6. Double marking catches examiner bias; moderation resolves genuine disagreement. *Steps.* # Change @RubricScore@ so several evaluators can score the same answer. The pair of answer plus evaluator must be unique Task H3 — one examiner, one set of marks. # Add to @AttemptAnswer@: @evaluationStatus@ (PENDING, ONE_DONE, TWO_DONE, MODERATION_NEEDED, FINAL), @finalMarks@, @moderatedBy@. # When the second examiner submits, compare the two totals. If they differ by more than a configurable threshold — start with 15 percent of the question's maximum — set MODERATION_NEEDED. If they agree, set @finalMarks@ to the average and the status to FINAL. # An examiner must *never* see the other examiner's marks while still marking. Filter them out of the API response by evaluator id. # Add @GET /admin/evaluations/moderation-queue@ and @PUT /admin/evaluations/{answerId}/moderate@, where a Super Admin or Reviewer sets the final mark and writes a short reason. # Only once the status is FINAL should the mark count towards the attempt total. *Done when.* Two examiners mark 12 and 13 out of 20 — the answer goes FINAL at 12.5 automatically. Two examiners mark 8 and 18 — chips on Practice Hub (M9 extra) Open [PracticeHome.tsx](Frontend/src/LMS/PracticeHome.tsx). Today it lands in the moderation queue and the attempt total does not move until a reviewer settles it. h3. H9 — Thursday 1 October: Fix invite-only notifications and add shows every exam reminders (M10, #101) — this closes M10 for Phase 1 *Goal.* Students invited to a private test actually receive with no filter. Add three buttons above the email, and everyone gets a reminder before the exam starts. *Why.* There grid: All, Free, Premium. When Free is a real bug here: @findAssignedCandidates@ returns an *empty list* for tests whose availability is INVITE, so invite-only tests email precisely nobody. Separately the Gantt asks for reminders and we selected, show only send mail at publish time and result time. *Steps.* # Create @entity/TestInvitation.java@: link to @AssessmentTest@, link to @Users@, @invitedAt@, @notifiedAt@. Add @POST /tests/{id}/invitations@ and @GET /tests/{id}/invitations@. # Fix @findAssignedCandidates@ in @AssessmentService@: segment === "FREE". Same idea for INVITE tests return Premium. Keep the invited users instead of an empty existing stream exam list. Leave PUBLIC and PRIVATE behaving as they do now. # Create @ReminderScheduler@ using Spring's <code>@Scheduled</code>, running every 15 minutes. Add <code>@EnableScheduling</code> to the application class. # On each run, find published tests starting within the next 24 hours, and again within the next hour, and email each assigned candidate who has not had that particular reminder. Record what was sent on the invitation row so nobody gets the same reminder twice. # Reuse @AssessmentMailService.send@. Do not build a second mail path. The console fallback when SMTP is missing must keep working so local development does not break. # Keep the subject and body text in one place so they are easy to change later. # *Do not* build SMS, WhatsApp or push notifications. Those stay deferred. *Done when.* Create an invite-only test, invite two students, publish, and both receive the email. Set a start time 50 minutes away and confirm the one-hour reminder goes out remove category work on the next run and does not repeat. /courses. h3. H10 — Friday Week 2 October: Institution and batch reports, then Phase 1 sign-off (M8, #99) (W10) — this closes M8 lock Premium content and Phase 1 show results *Goal.* An institution admin can see how a whole batch performed on a test and compare batches against each other. *Why.* Everything we report today is about one student's single attempt. A coaching centre needs to know which batch is behind and which topic the whole class is failing. This is the last Phase 1 row on the Gantt. *Steps.* # Task H4 — Stop Free users from starting Premium practice (M9 gate, #100) Add @GET /reports/tests/{testId}/summary@: number of candidates who attempted, average score, highest and lowest, average time, and topic-wise accuracy across everyone. # Add @GET /reports/batches/{batchId}/summary@ using the Batch entity from H5, plus a ranked list of students. # Add @GET /reports/institutions/{institutionId}/comparison@ returning one row per batch so batches sit side by side. # Do the arithmetic tiny helper in the database with JPQL aggregate queries. *Do not* load every attempt into Java memory and loop — that will not survive real data volumes. # Enforce access strictly. Institution Admin reads only their own institution. Super Admin reads anything. A Candidate reads none of these. # Spend the rest of the day on Phase 1 sign-off. Run every endpoint you built in W11 and W12 once, then write a short comment on #95, #96, #97, #98, #99, #100 and #101 saying exactly what shipped and what is still deferred. [auth.ts](Frontend/src/utils/auth.ts): *Done when.* Two batches take the same test and the comparison endpoint shows two rows with averages you can verify by hand from the raw attempt rows. h1. Week 13 — Mon 5 to Fri 9 October Phase 2 opens. Two Gantt tracks start: M11 Language Assessment, and QB-1 the JEE Main question bank. *Read this before you start.* The audit found that CAT has about *10,092 questions* across 36 chapter files in @backend/data/cat_questions_json/@, but JEE Main has isPremiumPlan(plan) returns true only *9 sample questions* in @backend/data/jee_questions_json/@. The Gantt wants 24,000 unit-wise JEE Main questions. Our only import script, @backend/scripts/convert_and_import_cat_questions.py@, when plan is hardcoded to CAT. So this week is about *building the machinery*, not typing questions. Writing the actual content stays with Ayush Sharma on the Gantt. h3. H11 — Monday 5 October: One import script that works for every exam (QB-1, #112) *Goal.* A single script that imports questions into any of our six exam banks, from CSV MONTHLY, QUARTERLY, ANNUAL, or JSON. *Why.* The CAT script has the CAT folder path, CAT chapter names and CAT subject mapping baked in. Copying it five more times would leave six scripts to maintain and six places for the same bug. We need one script driven by ENTERPRISE. When a config file. *Steps.* # Create @backend/scripts/import_questions.py@, starting from the existing CAT script so the working parts are preserved. # Take arguments: @--exam@ (JEE_MAINS, JEE_ADVANCED, NEET, CAT, JIPMAT, CUET), @--input@ (file or folder), @--mapping@ (config file), @--dry-run@, @--import@. # Move the filename-to-chapter and subject mapping into Free user clicks a JSON config per Premium exam under @backend/scripts/mappings/@. Adding or a new exam then means adding a config file, paid mock, do not editing code. # Support both formats. CSV gets a column mapping in start the config. JSON is expected in the @{exam, questions}@ shape the import endpoint already accepts. # Validate every row before sending anything: content not empty, at least two options, correct answer must be one of the options, marks and negative marks must be numbers. Collect *all* the problems and print a report. Do not stop at the first bad row. # @--dry-run@ must be the *default*. Nobody should write 4,000 rows into the database by accident. # test. Send in batches of 500 them to @POST /api/exam-questions/import@, with a progress line per batch. One request with 24,000 questions will time out. # Skip duplicates — the repositories already have @existsByContentIgnoreCase@. Report how many were skipped. # Write a summary file next to the input, the way the CAT script writes @_import_summary.json@. *Done when.* Running the script against @backend/data/jee_questions_json/@ in dry-run reports 9 valid questions and no errors. Running with @--import@ loads them, and running it again skips all 9 as duplicates. h3. H12 — Tuesday 6 October: Maths formulas and images on questions (M2, #93) *Goal.* A question /pricing. Premium users can contain a properly typeset mathematical formula start both Free and a diagram. *Why.* We cannot build a serious JEE or NEET bank without this. Physics and Maths questions are full of integrals, fractions and vectors, and many refer to a circuit diagram or a graph. Today @content@ is plain text and there is *no image field anywhere*, so those questions simply cannot be stored. *Steps.* # Add to @ExamQuestionBase@: @contentFormat@ (PLAIN or LATEX, default PLAIN), @imageUrl@, @solutionImageUrl@. # Add a latex flag and an image URL to each *option* too, since options often contain formulas. Options are stored as a JSON list, so extend that structure rather than adding columns. # Add the same fields to @ExamQuestionItemRequest@ so the importer can populate them. # Add @POST /admin/questions/image@, reusing the safe file handling from H6: whitelist content types, UUID filename, size cap. # Store LaTeX *exactly* as given. Do not parse or clean it on the Premium items. Nikita’s server — the browser library renders it, and touching it here will only corrupt formulas. # Update @ExamQuestionResponse@ so the new fields reach the frontend. # Because @ddl-auto=update@ adds columns automatically, confirm existing rows still work with @contentFormat@ defaulting to PLAIN. *Done when.* Save a question with @contentFormat@ LATEX and content like @\int_0^1 x^2 dx@, attach a diagram, read it back through @GET /api/exam-questions@, and everything is intact. h3. H13 — Wednesday 7 October: Generate many practice already blocks paid tests from a question pool (M3 and QB-1, #94 and #112) *Goal.* One API call produces many different practice papers from an exam bank, following a blueprint. *Why.* The Gantt asks for 100 practice tests and 100 mock tests *per exam*. Nobody will assemble 200 papers by hand in Freemium. Harshit only handles the admin panel. What we have today is @ExamQuestionService.sample@, which shuffles and takes N — that ignores subject balance and difficulty, so one paper might be all easy Chemistry. A real paper needs a blueprint. *Steps.* # Create @services/TestGenerationService.java@. # Add @POST /tests/generate@ taking: the exam bank, how many tests, a name pattern like @JEE Main Practice Test {n}@, the duration, and a blueprint. # The blueprint is a list of rules. Each rule says subject, optional chapter, difficulty, and how many questions. For example 25 Physics medium, 25 Chemistry easy, 25 Maths hard. # Before generating anything, check there are enough questions for every rule. friendly redirect. If Physics-hard has 20 and the blueprint wants 25, fail immediately naming the shortfall. Do not generate half a paper. # Make the papers genuinely different. Track which questions each paper used and cap how much any two papers may overlap — start at 20 percent. # Create real @AssessmentTest@, @TestSection@ and @TestQuestion@ rows, so generated papers behave exactly like hand-made ones everywhere else. # Accept an optional random seed so a run can be reproduced when debugging. # Run the whole thing in one transaction and return a summary: how many papers, their ids, and any warnings. # Restrict to Super Admin and Test Admin. It creates a lot of data. *Done when.* Generating 10 practice tests from the CAT bank with a VARC / DILR / QA blueprint produces 10 tests, each matching the blueprint exactly, with no two sharing more than a fifth of their questions. h3. H14 — Thursday 8 October: Audio for listening questions (M11, #102) *Goal.* A question can have an audio clip attached, and the server controls how many times a student may play it. *Why.* This is the first half of M11 Language Assessment, which starts this week. The play-count rule matters: in real language exams the audio plays a fixed number of times, and if we enforce that only in the browser a student can just reload the page. *Steps.* # Add to @ExamQuestionBase@: @audioUrl@, @audioDurationSeconds@, @maxPlayCount@ (null meaning unlimited). # Add @POST /admin/questions/audio@ accepting MP3 and M4A up to 20 MB, with the same safe-filename and content-type checks as H6. # Serve audio through @GET /content/audio/{id}@. Support HTTP range requests so the browser can stream and seek, otherwise large files will not play reliably. # Track plays *server-side*. Add a @PlaybackLog@ recording attempt, question and time of each play. Add @POST /attempts/{id}/questions/{qid}/play@ which the player calls before each play; it API returns how many plays remain, or an error at the limit. # Return the remaining play count in the attempt response so the player can show it from the start. # Only allow audio access to a student with an active attempt containing that question. Do not leave exam audio publicly downloadable. *Done when.* Upload a clip, set @maxPlayCount@ to 2, and the third play request is rejected by the server. Refreshing the page does not reset the counter. h3. H15 — Friday 9 October: Recording answers for speaking tests (M11, #102) *Goal.* A student can record a spoken answer and have it stored for an examiner to mark. *Why.* This pairs with the listening work and completes the listening-and-speaking pair the Gantt schedules for W13 to W14. Nikita builds the microphone recorder the same week, so *agree the audio format and field names with her first thing in the morning*. *Steps.* # Add a SPEAKING value to the question type list on @ExamQuestionBase@, plus @maxRecordingSeconds@ and @preparationSeconds@. # Extend @AnswerAttachment@ from H6 to accept @audio/webm@ and @audio/mp4@, which is what browser recording produces. Cap at 25 MB. # Add @POST /attempts/{attemptId}/answers/{testQuestionId}/recording@ to receive the audio plus its length in seconds. # Reject recordings longer than @maxRecordingSeconds@ plus a small tolerance. # Only one recording per question. A second upload replaces the first and deletes the old file, so nobody is confused about which one counts. # Add @GET /admin/evaluations/{answerId}/recording@ so an examiner can listen while marking. Reuse the rubric scoring from H7 — speaking is marked against criteria like fluency and pronunciation, exactly like written work. # *Do not* attempt automatic speech scoring. That is M17 in Phase 3 and is out of scope. *Done when.* Nikita's recorder uploads a 30-second clip, paid plan, show it appears in the examiner panel, plays back cleanly, and an examiner can score it against a rubric. h1. Week 14 — Mon 12 link to Fri 16 October Plans. Theme: M12 Adaptive Testing starts on the Gantt, M11 continues, and the first real JEE Main practice papers get generated. Friday is a quality day, because the whole backend currently has only *two* test files. h3. H16 Task H5 — Monday 12 October: Groundwork for adaptive tests (M12, #103) *Goal.* The system understands a new kind of test where the next question depends on how the student is doing. *Why.* M12 runs W14 to W18 and this is its first week. An adaptive test has *no fixed question list*, Candidate Results page (M8 start, #99) Ask Nikita which breaks several of our assumptions — so lay the foundations carefully before writing the algorithm tomorrow. *Steps.* # Add ADAPTIVE URL to the @testType@ values on @AssessmentTest@, alongside PRACTICE, MOCK, EXAM and ASSIGNMENT. # Add settings on the test: @adaptiveQuestionBank@, @adaptiveTotalQuestions@, @adaptiveStartDifficulty@, @adaptiveStopOnConfidence@. # Add to @AssessmentAttempt@: @abilityEstimate@ (decimal, starts at 0), @abilityStandardError@, @currentDifficulty@. # Make sure every difficulty value in the six exam banks maps to a number. *Check what strings are actually in the data first* with a query, then map them, for example easy to -1, medium to 0, hard to 1. Do not assume the data is clean. # For an adaptive attempt, do *not* create all the @TestQuestion@ rows at the start. call. It should be GET http://localhost:8081/api/reports/attempts/{id} (already exists). Create them one at a time as questions are served, because we do not know in advance which questions the student will see. # Confirm scoring, the audit log and the reports all still work when the question list grows during the attempt. This is the part most likely to break. *Done when.* You can create an adaptive test and start an attempt. Only one question exists at the start and the ability estimate is 0. Nothing about normal fixed tests changes. h3. H17 — Tuesday 13 October: Choosing the next question (M12, #103) *Goal.* After each answer the system picks a harder or easier question based on how the student is performing. *Why.* This is the core of adaptive testing. The principle is simple: a right answer means the student is probably stronger than we thought, so ask something harder; a wrong answer means ask something easier. Done well this finds a student's true level in far fewer questions than a fixed paper. *Steps.* # Create @services/AdaptiveTestService.java@. # Add @GET /attempts/{id}/next-question@. It scores the previous answer, updates the ability estimate, picks the next question, creates its @TestQuestion@ row, and returns it. # Start simple and correct. *Do not* attempt Item Response Theory — Results page (for example /results/:attemptId) that is M14 in a later phase. Use a step rule: correct moves the target difficulty up one step, wrong moves it down one step, shows score, rank, percentile, time, and the steps get smaller as the estimate settles. # Pick the question whose difficulty is closest to the target, topic breakdown from the configured bank, excluding anything already seen in this attempt. If several tie, choose randomly so two students do not get identical papers. # If the bank runs out at the target difficulty, widen the search rather than failing. If it runs out entirely, end the test early and record why. # Stop when either the question count is reached, or the standard error drops below the confidence threshold. # *Never* send the correct answer or the solution in the next-question response. This endpoint is called from the browser and a student can read the response. # Write an audit row for every question served, with the difficulty and the ability estimate at that moment, so we can debug why JSON. No charts library. No PDF download. Add a student got “View result” link from the paper they got. *Done when.* Answer everything correctly and the difficulty climbs steadily. Answer everything wrongly and it falls. Answer randomly and it settles near the middle. No question repeats within practice/test finish screen if one attempt. h3. H18 — Wednesday 14 October: Turning the ability estimate into already exists; otherwise a score (M12, #103) *Goal.* An adaptive attempt produces a meaningful, comparable score. *Why.* Adaptive scores cannot be "marks out small list of total". Two students answering 20 questions each may have faced completely different papers, so 15 out of 20 on a hard path is a much better performance than 18 out of 20 on an easy one. The score has to reflect the difficulty of the questions faced. *Steps.* # In @AssessmentService.evaluate@, branch for ADAPTIVE tests. # Convert the final ability estimate into a scaled score on a fixed range, for example 0 to 100, so all adaptive recent attempts are comparable regardless of path. # Weight each question by its difficulty when computing its contribution, so a correct hard answer is worth more than a correct easy one. # Store *both* numbers: enough. Register the scaled score route in @score@, and the raw correct count separately, so reports can show both. # Make @recomputeRanks@ work for adaptive attempts by ranking on the scaled score. # Add the ability estimate and its standard error to the report response, so the result page can show App.tsx. Do not add a confidence band rather than a single misleadingly precise number. # Write the formula and the reasoning as a comment in the service. The next person to read this needs to understand why the arithmetic is what Header item unless it is. *Done when.* Two students each answer 15 of 20 correctly, one on a hard path and one on an easy path, and the hard-path student scores measurably higher. Ranks compute without error. h3. H19 — Thursday 15 October: Generate the first real JEE Main practice papers (QB-1, #112) *Goal.* Use the importer from H11 and the generator from H13 together and produce genuine practice papers. *Why.* This is the first end-to-end proof that our question-bank pipeline works. It will also expose whatever is wrong with both tools while they are still fresh in your mind, which is far cheaper than finding out during a 24,000-question import. useful. *Steps.* # Talk to Ayush Sharma first and get whatever JEE Main Physics Class XI content exists. *Do not invent questions yourself.* # Write the JEE Main mapping config under @backend/scripts/mappings/@, following the CAT config as a model. Map unit names to chapters and to Physics, Chemistry and Mathematics. # Run the importer in dry-run first and fix every validation problem it reports. Expect problems Task H6 — real content is always messier than sample data. # Import for real, then verify by querying @jee_mains_questions@ directly: count rows, check the subject and chapter spread, and read ten questions end to end to confirm nothing was mangled. # Build a practice blueprint matching the real JEE Main pattern: 25 questions per subject with a realistic easy / medium / hard mix. Look the pattern up rather than guessing. # Generate *10* practice tests, not 100. Start small so mistakes are cheap. # Take one Polish (end of the generated papers yourself, all the way to the result page. This is the real test of whether the chain works. # Write what you found, with counts and any problems, as a comment on #112. We need this before scaling to 24,000 questions. W10) *Done when.* 10 JEE Main practice tests exist, each matching the blueprint, and you have personally completed one and seen a correct result page. h3. H20 — Friday 16 October: Automated tests for the exam engine (M4 and M5, #95 and #96) *Goal.* Automated tests that will catch it if someone breaks scoring or attempt handling. *Why.* The whole backend has exactly *two* test files: @BackendApplicationTests.java@ and @rules/TopicAccessRulesTest.java@. Scoring is the most important code we have — if it silently breaks, students get wrong marks and we may not notice for weeks. You have changed scoring, timing, navigation and payments over four weeks. Lock that behaviour down. *Steps.* # Create @AssessmentServiceTest@ covering @gradeObjective@ thoroughly: correct single MCQ, wrong one with negative marks, fully correct multi-correct with bonus, partially correct multi-correct, multi-correct with a wrong option selected, numerical within tolerance, numerical outside it, and numerical inside the new range from H4. # Add tests for the time limit from H2: saving before the deadline works, saving after it auto-submits and errors. # Add tests for the navigation rules from H3: sequential rejects jumping ahead, section-locked rejects going back. # Add a test for the payment signature check from H1: valid signature passes, invalid one is rejected, and the plan does not change Fix spacing on rejection. Use a *fixed test key*, never a real one. # Use an in-memory H2 database so tests run without MySQL and anyone can run them. # Confirm @mvn test@ passes and add the command to the README. # Aim for meaningful coverage of @AssessmentService@, not a coverage percentage. Test the paths that would hurt if they broke. *Done when.* @mvn test@ runs green, and deliberately breaking the negative-marking line makes a test fail with a clear message. h1. Do not do these Say no if they come up. They are either scheduled for a later Gantt phase or explicitly deferred Plans page, empty filter state (“No Premium exams in our project rules. * Google this list”), and Microsoft sign-in, make sure Courses and hardware device binding. * Live PayU, Stripe and PayPal. Razorpay stays in stub mode locally; only Practice Hub both show the signature check becomes real. * SMS, WhatsApp and push notifications (M10). Email only. * Camera-based proctoring. * Item Response Theory and the question analytics dashboard (M14, W15 to W19). * White-label branding and custom domains (M15, W17 to W20). * AI-assisted marking (M17, Phase 3). * Multi-tenant architecture and the 10,000 concurrent user load test (M18 and M19, Phase 3). * The coding assessment module (M20, optional). * Writing the bulk question content itself. Per the Gantt the question bank tracks belong to Ayush Sharma — you build the tooling. same badge.