Applications & Onboarding (incl. IIMT)¶
This domain covers how a prospect becomes an enrolled MAS student: the generic application intake (the multi-step "apply to MAS" wizard backed by the applications table), the entity-subscriber side-effects that turn a fresh application into a CRM lead, the free pre-enrollment "application / counselling" meetings booked against application mentors (with WhatsApp reminders), the standalone IIMT partner-campaign application flow (WhatsApp-OTP gated), and the admin "Provision Mentee" tool that fast-tracks a fully-enrolled student in one call. It is the front door of the funnel and feeds the Sales CRM, Mentorship/Meetings, and LMS/Courses domains.
Status: documented from source on this branch.
Overview¶
The Applications & Onboarding domain sits at the boundary between an anonymous/lead prospect and a provisioned, enrolled student. It is intentionally several smaller flows rather than one monolith:
- Generic application intake — an authenticated student fills the application wizard (personal details, college/profession, batch selection, documents). Each save/submit reads and writes the
applicationsrow owned by the student. Implemented insrc/controllers/student.controller.ts(submit/update/get/upload-documents) andsrc/services/ApplicationService.ts. - Application meetings (free counselling sessions) — before/while applying, a student can book a short free meeting with an "application mentor" (a flagged
EXPERT). No tokens are deducted, the slot auto-confirms, and a WhatsApp reminder is scheduled. Implemented insrc/controllers/applicationMeeting.controller.ts,src/services/ApplicationMeetingService.ts, andsrc/services/ApplicationMeetingReminderService.ts. - IIMT application flow — a separate, public partner-campaign intake (the
iimt-space-techfront end) gated by a WhatsApp OTP, persisting to its owniimt_applicationstable. Implemented insrc/controllers/iimt.controller.tsandsrc/entities/IIMTApplication.entity.ts. - Mentee provisioning — an admin/CSV tool that creates (or converts) a verified user plus an
ENROLLEDapplication with every onboarding milestone flag flipped true, landing the person directly on the enrolled dashboard. Implemented insrc/controllers/menteeProvision.controller.ts.
Personas¶
| Persona | Touchpoint |
|---|---|
Prospect / student (USER) |
Application wizard, document upload, booking a free application meeting |
Application mentor (EXPERT with isApplicationMentor = true) |
Receives meeting booking + is the counsellor on the free slot |
Admin / Batch Lead (ADMIN / SUPERADMIN) |
Toggles application mentors, provisions mentees, reviews applications |
| IIMT campaign applicant (anonymous, public) | WhatsApp-OTP verified IIMT form |
Place in the suite¶
An inserted Application automatically generates a Lead (see src/subscribers/application.subscriber.ts), wiring intake into the Sales CRM domain. Free meetings reuse the Mentorship & Meetings Slots machinery (same table, source = 'application', isFree = true). Batch/course selection and the enrolled dashboard tie into the Education / LMS / Courses domain. The IIMT flow is self-contained and only shares the WhatsApp sending infrastructure.
Key concepts & entities¶
Glossary
- Application — the student's enrollment record. One student may have several rows over time (drafts, paid, enrolled); code carefully prefers an active application over a chronologically-newer draft.
- Editable application — the latest
DRAFTorPAIDrow; document uploads and edits target this so they do not land on a stale row (ApplicationService.getEditableApplicationByUserId). - Onboarding milestone flags — booleans on the application (
diagnosticCompleted,meetingCompleted,paymentVerified,mouSigned,onboardingFeePaid) that gate the enrolled dashboard. The dashboard reads these from theapplicationsrow, not fromusers. - Application mentor — an
EXPERTuser whoseMentor.isApplicationMentorflag istrue, eligible to host free application meetings. - Free application slot — a
Slotsrow withisFree = true,source = 'application', auto-confirmed and token-free. - Reminder login token — a short-lived opaque token (Redis) embedded in the WhatsApp reminder deep-link that, when redeemed, mints a JWT + refresh token so the student lands logged-in directly in the meeting.
- IIMT application — partner-campaign lead captured via WhatsApp OTP, stored separately.
- Provision Mentee — admin shortcut that creates a fully-enrolled student record in one POST.
Main entities
| Entity | File | Table | Notes |
|---|---|---|---|
Application |
src/entities/Application.ts |
applications |
Core enrollment record. status enum ApplicationStatus. Holds personal details, batch/course, documents JSON, milestone flags, Razorpay payment fields, source. |
IIMTApplication |
src/entities/IIMTApplication.entity.ts |
iimt_applications |
Standalone: name, whatsappNumber, passingYear, isVerified. |
ApplicationWhatsAppMessage |
src/entities/ApplicationWhatsAppMessage.entity.ts |
application_whatsapp_messages |
Audit/log of outbound (and inbound) Meta Cloud WhatsApp messages tied to an application — used by the meeting reminder send and retried from a failures portal. |
Lead |
src/entities/Lead.ts |
(CRM) | Auto-created by ApplicationSubscriber.afterInsert. |
Slots |
src/entities/Slots.ts |
slots |
Reused for free application meetings (source = 'application'). |
Mentor |
src/entities/Mentor.ts |
— | isApplicationMentor flag selects application mentors. |
User / Token |
src/entities/User.ts, src/entities/Tokens.ts |
— | UserSubscriber.afterInsert seeds a zero-balance Token for every new user. |
Architecture¶
flowchart TD
subgraph Clients["Clients"]
SW["Student wizard (mas-website / frontend)"]
IIMTUI["IIMT campaign site (iimt-space-tech)"]
ADMINUI["Admin panel (Provision Mentee + mentor toggles)"]
end
subgraph Routes["Express routes"]
SR["/api/student/application*"]
AMR["/api/application-meetings/*"]
AMA["/api/admin/application-meetings/*"]
IR["/api/iimt/*"]
MR["/api/admin/mentees/*"]
end
subgraph Controllers["Controllers"]
SC["student.controller.ts"]
AMC["applicationMeeting.controller.ts"]
IC["iimt.controller.ts"]
MPC["menteeProvision.controller.ts"]
end
subgraph Services["Services"]
AS["ApplicationService.ts"]
AMS["ApplicationMeetingService.ts"]
AMRS["ApplicationMeetingReminderService.ts"]
WAS["WhatsAppService.ts"]
QS["QueueService.ts"]
end
subgraph Data["Entities / DB (PostgreSQL)"]
APP["applications"]
IIMT["iimt_applications"]
SLOTS["slots"]
LEAD["leads"]
AWM["application_whatsapp_messages"]
USR["users / tokens / mentors"]
end
subgraph Subs["TypeORM subscribers"]
APPSUB["ApplicationSubscriber (afterInsert)"]
USRSUB["UserSubscriber (afterInsert)"]
end
subgraph Ext["External / async"]
REDIS["Redis (OTP + reminder tokens)"]
BULL["BullMQ whatsappQueue + emailQueue"]
META["Meta Cloud WhatsApp API"]
MAIL["Email (Nodemailer)"]
end
SW --> SR --> SC
ADMINUI --> AMA --> AMC
ADMINUI --> MR --> MPC
SW --> AMR --> AMC
IIMTUI --> IR --> IC
SC --> AS --> APP
SC --> APP
AMC --> AMS --> SLOTS
AMS --> QS --> BULL
AMS --> AMRS
AMRS --> REDIS
AMRS --> WAS --> AWM
AMRS --> QS
BULL --> AMRS
AMRS --> META
AMS --> MAIL
IC --> REDIS
IC --> WAS --> META
IC --> IIMT
MPC --> USR
MPC --> APP
APP -.afterInsert.-> APPSUB --> LEAD
USR -.afterInsert.-> USRSUB --> USR
Data model¶
erDiagram
USER ||--o{ APPLICATION : "submits"
USER ||--o| TOKEN : "owns"
USER ||--o| MENTOR : "may have profile"
BATCH ||--o{ APPLICATION : "selected by"
APPLICATION ||--o{ APPLICATION_WHATSAPP_MESSAGE : "logs"
APPLICATION ||--o| LEAD : "auto-creates"
MENTOR ||--o{ SLOTS : "offers"
USER ||--o{ SLOTS : "books as student"
APPLICATION {
uuid id PK
uuid userId FK
string fullName
string phone
string college
boolean isIITCollege
int graduationYear
string targetProfile
uuid batchId FK
string batchCode
uuid courseId
boolean courseOverridden
json documents
enum status
boolean diagnosticCompleted
boolean meetingCompleted
boolean paymentVerified
boolean mouSigned
boolean onboardingFeePaid
string masRollNumber UK
boolean placed
decimal finalAmount
string razorpayOrderId
string source
timestamp submittedAt
timestamp reviewedAt
}
IIMT_APPLICATION {
uuid id PK
string name
string whatsappNumber
string passingYear
boolean isVerified
timestamp createdAt
}
APPLICATION_WHATSAPP_MESSAGE {
uuid id PK
uuid applicationId FK
uuid userId FK
string candidatePhone
string direction
string status
string externalMessageId
jsonb requestPayload
int retryCount
}
SLOTS {
uuid id PK
uuid mentorId FK
uuid studentId FK
boolean isFree
string source
enum status
timestamp startDateTime
timestamp endDateTime
}
LEAD {
uuid id PK
uuid userId FK
string source
enum status
string notes
}
Notable enums / status values
ApplicationStatus(src/entities/Application.ts):draft,submitted,under_review,approved,rejected,payment_received,paid,batch_allocated,enrolled,deboarded.Application.source:quick_pay|application_wizard|admin|lead_conversion(defaultapplication_wizard).ApplicationWhatsAppMessage.status:queued|submitted|sent|delivered|read|failed;direction:outbound|inbound.Slotsfor application meetings always carryisFree = true,source = 'application', andstatus = CONFIRMEDonce booked.
API surface¶
Paths below are derived from the actual route files plus their mount prefixes in src/routes/index.ts (/api, /api/admin) and src/routes/student.routes.ts (mounted at /api/student) and src/routes/admin.routes.ts (mounted at /api/admin).
Generic application intake — src/routes/student.routes.ts (mounted /api/student)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| POST | /api/student/application |
auth (student) | Submit / update the application (sets status submitted). Validates batch document requirements, blocks a second active application for a different batch. |
| GET | /api/student/application |
auth (student) | Fetch the student's application (prefers the active one over a stray draft). |
| PATCH | /api/student/application |
auth (student) | Partial update of an editable (draft/paid) application (batch, phone, college, etc.). |
| PATCH | /api/student/application/documents |
auth (student) | Upload/replace documents on the latest non-rejected application without changing status. |
Application meetings (public + student) — src/routes/applicationMeeting.routes.ts (mounted /api)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/application-meetings/status |
public | Feature flag + maxFreeMeetings + meetingDurationMinutes. |
| GET | /api/application-meetings/reminder-login |
public (token in query) | Redeem the reminder deep-link token → returns JWT + refresh token + meeting id. |
| GET | /api/application-meetings/mentors |
auth | List application mentors (gated by feature flag). |
| GET | /api/application-meetings/remaining |
auth | { used, max, remaining } free meetings for the student. |
| GET | /api/application-meetings/active-check |
auth | Whether the student has a confirmed, not-yet-ended free meeting. |
| POST | /api/application-meetings/book |
auth | Book a free slot (mentorId, slotId); auto-confirm, no token charge. |
Application meetings (admin) — src/routes/applicationMeeting.routes.ts (mounted /api/admin)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| PUT | /api/admin/application-meetings/mentor/:userId |
auth + admin | Toggle a user's isApplicationMentor flag. |
| POST | /api/admin/application-meetings/mentors/bulk |
auth + admin | Bulk set application mentors by email list. |
| GET | /api/admin/application-meetings/mentors |
auth + admin | List application mentors with free-booking stats. |
IIMT flow — src/routes/iimt.routes.ts (mounted /api)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| POST | /api/iimt/send-otp |
public | Generate 4-digit OTP, store in Redis (5 min TTL), send via WhatsApp. |
| POST | /api/iimt/verify-otp |
public | Verify OTP, set verified flag (30 min), return existing IIMTApplication if the number is known. |
| POST | /api/iimt/apply |
public (verified gate) | Persist the IIMT application (requires a valid verified flag). |
Mentee provisioning — src/routes/admin.routes.ts (mounted /api/admin)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/admin/mentees/batches |
auth + admin | Batch dropdown options for the provisioning UI. |
| POST | /api/admin/mentees/provision |
auth + admin | Create/convert a user and write an ENROLLED application with all milestone flags true. |
Note: discovered alongside but out of scope for this doc:
jobApplication.routes.ts(Mr. Hire candidate job applications) andmentorApplication.routes.ts(mentor onboarding). They are distinct domains despite the similar "application" wording.
User journeys¶
1. Prospect submits an application (wizard)¶
The student fills the wizard and submits. The controller resolves the batch, enforces document requirements, prevents a conflicting second application, merges documents (preserving s3Key), flips status to submitted, and marks the user profile complete.
sequenceDiagram
actor S as Student
participant FE as Frontend wizard
participant SC as student.controller
participant DB as applications table
participant U as users table
S->>FE: Fill personal details, pick batch, attach docs
FE->>SC: POST /api/student/application
SC->>DB: Look up batch by id or code
SC->>DB: Check for active application on a DIFFERENT batch
alt Conflicting active application
SC-->>FE: 409 complete or withdraw the other application
else No conflict
SC->>DB: Find existing app for same batch or any draft
SC->>SC: Merge documents and keep existing s3Key
alt Batch requires documents and some are missing
SC-->>FE: 400 with missingDocuments list
else Documents satisfied
SC->>DB: Save application status submitted and submittedAt now
SC->>U: Update profile fields and set isProfileComplete true
SC-->>FE: 200 application submitted
end
end
2. Application subscriber side-effect (auto lead creation)¶
Whenever any Application row is inserted (wizard, provisioning, lead conversion), the TypeORM subscriber creates a CRM Lead for that user if one does not already exist for the my-analytics-school source. The new-user UserSubscriber separately seeds a zero-balance token wallet.
sequenceDiagram
participant ORM as TypeORM
participant ASUB as ApplicationSubscriber
participant LR as leads table
participant USUB as UserSubscriber
participant TR as tokens table
ORM->>ASUB: afterInsert(Application)
ASUB->>ASUB: Guard return if no userId
ASUB->>LR: Find lead for userId and source my-analytics-school
alt Lead already exists
ASUB-->>ORM: Do nothing
else No lead yet
ASUB->>LR: Save new ACTIVE lead noting application id
end
Note over USUB,TR: On a brand new User insert (parallel concern)
ORM->>USUB: afterInsert(User)
USUB->>TR: Find token for user
alt No token yet
USUB->>TR: Create token with balance 0
end
3. Book a free application/counselling meeting + schedule reminder¶
The student picks an application mentor and a 15-minute free slot. The service gates on the feature flag, validates the mentor flag, blocks if an active meeting exists, enforces the free cap and exact duration, books the slot token-free, emails both parties, and schedules a delayed WhatsApp reminder job.
sequenceDiagram
actor S as Student
participant AMC as applicationMeeting.controller
participant AMS as ApplicationMeetingService
participant SL as slots table
participant Q as QueueService
participant EM as emailQueue
participant WQ as whatsappQueue
S->>AMC: POST /api/application-meetings/book mentorId slotId
AMC->>AMS: bookFreeSlot
AMS->>AMS: Check feature enabled and not expired
AMS->>SL: Validate mentor is an application mentor
AMS->>SL: Check for an active confirmed free meeting
alt Active meeting already in progress
AMS-->>AMC: fail complete current meeting first
else No active meeting
AMS->>SL: Check remaining free meetings under cap
AMS->>SL: Find available future slot with matching duration
alt Slot invalid or wrong duration
AMS-->>AMC: fail with reason
else Slot OK
AMS->>SL: Assign student, mark unavailable, status CONFIRMED, source application
AMS->>Q: addEmailJob APPLICATION_MEETING_BOOKED to student
Q->>EM: enqueue student email
AMS->>Q: addEmailJob APPLICATION_MEETING_BOOKED_MENTOR to mentor
Q->>EM: enqueue mentor email
AMS->>Q: scheduleReminder via addWhatsAppJob with delay
Q->>WQ: enqueue APPLICATION_MEETING_REMINDER delayed
AMS-->>AMC: success with slot
end
end
4. WhatsApp reminder fires + one-click login¶
The delayed job fires reminderLeadMinutes before start. The worker re-checks the slot is still confirmed and the student opted into WhatsApp, mints a Redis-backed login token, and sends a Meta Cloud template message whose button carries a deep link. Redeeming that link logs the student straight into the meeting.
sequenceDiagram
participant WQ as whatsappQueue (BullMQ)
participant WW as whatsapp.worker
participant RS as ReminderService
participant DB as slots and applications
participant PR as NotificationPreferenceService
participant R as Redis
participant WA as WhatsAppService
participant META as Meta Cloud API
actor S as Student
WQ->>WW: Deliver APPLICATION_MEETING_REMINDER slotId
WW->>RS: processReminderJob slotId
RS->>DB: Load slot with mentor and user relations
alt Slot not confirmed or no phone
RS-->>WW: Return without sending
else Slot still valid
RS->>PR: allowsWhatsApp studentId
alt Student opted out of WhatsApp
RS-->>WW: Skip and log opt out
else WhatsApp allowed
RS->>DB: Load latest application for student
RS->>R: Store reminder login token with TTL
RS->>WA: sendApplicationMessage template with body and url button
WA->>META: POST template message
WA->>DB: Log row in application_whatsapp_messages
end
end
Note over S,META: Later, student taps the reminder button
S->>RS: GET /api/application-meetings/reminder-login token
RS->>R: Look up token payload
RS->>DB: Validate slot belongs to user and user active
RS-->>S: JWT plus refresh token plus meetingId
5. IIMT application (WhatsApp-OTP gated)¶
A public campaign visitor verifies their WhatsApp number via OTP, then submits the IIMT form. Verification state lives entirely in Redis; an existing application is surfaced for "smart detection".
sequenceDiagram
actor V as Campaign visitor
participant IC as iimt.controller
participant R as Redis
participant WA as WhatsAppService
participant DB as iimt_applications
V->>IC: POST /api/iimt/send-otp whatsapp
IC->>R: Set iimt:otp:number 4-digit OTP TTL 5 min
IC->>WA: sendMessage OTP body
alt WhatsApp send fails
WA-->>IC: error then log fallback OTP to server console
end
IC-->>V: 200 OTP sent
V->>IC: POST /api/iimt/verify-otp whatsapp otp
IC->>R: Get stored OTP and compare
alt OTP missing or mismatch
IC-->>V: 400 invalid or expired OTP
else OTP matches
IC->>R: Set iimt:verified:number TTL 30 min and delete OTP
IC->>DB: Find existing application by whatsappNumber
IC-->>V: 200 verified with isExistingUser flag
end
V->>IC: POST /api/iimt/apply name whatsapp passingYear
IC->>R: Check iimt:verified flag
alt Not verified
IC-->>V: 403 verify your number first
else Verified
IC->>DB: Save new IIMTApplication isVerified true
IC->>R: Delete verified flag
IC-->>V: 201 application submitted
end
6. Convert an applicant into a provisioned mentee (admin)¶
The admin tool creates or converts a user to a verified, profile-complete student and writes (or updates) an ENROLLED application with every onboarding flag true, so the person lands directly on the enrolled dashboard. Converting an existing email requires explicit confirmation.
sequenceDiagram
actor A as Admin
participant MPC as menteeProvision.controller
participant U as users table
participant B as batches table
participant DB as applications table
A->>MPC: POST /api/admin/mentees/provision name email password batchId
MPC->>MPC: Validate email, password length, batchId present
MPC->>B: Load batch
alt Batch not found
MPC-->>A: 400 batch not found
else Batch OK
MPC->>U: Find user by lower email
alt Existing user and confirmRoleChange not set
MPC-->>A: 200 needsConfirm with existing user summary
else Create or confirmed conversion
alt New user
MPC->>U: Create verified profile-complete active student
else Existing user
MPC->>U: Update password role user stage 3 verified active
end
MPC->>DB: Find latest application for user
alt No application
MPC->>DB: Insert ENROLLED application all milestone flags true
else Application exists
MPC->>DB: Update to ENROLLED and set all milestone flags true
end
MPC-->>A: 200 enrolled true with userId and batchCode
end
end
Background jobs & async¶
| Mechanism | Where | Trigger | Behaviour |
|---|---|---|---|
whatsappQueue (BullMQ) |
src/services/ApplicationMeetingReminderService.ts → scheduleReminder; processed by src/workers/whatsapp.worker.ts |
On free-meeting booking | A delayed job (delay = startTime − leadMinutes) with a deterministic jobId application-meeting-reminder-{slotId}. The worker calls processReminderJob. removeOnComplete/removeOnFail = 100. Concurrency 5. |
emailQueue (BullMQ) |
src/services/ApplicationMeetingService.ts via QueueService.addEmailJob |
On free-meeting booking | Two emails: APPLICATION_MEETING_BOOKED (student) and APPLICATION_MEETING_BOOKED_MENTOR (mentor). Failures are caught and logged, not fatal. |
| Reminder re-validation | processReminderJob |
At reminder fire time | Skips send if slot is no longer CONFIRMED, has no phone, the student opted out of WhatsApp (NotificationPreferenceService.allowsWhatsApp), or no application exists. |
| Reminder login token | ApplicationMeetingReminderService.createReminderLoginToken |
At reminder send | Opaque crypto.randomBytes(18).base64url token stored in Redis (application_meeting_reminder_token:{token}) with TTL ≥ slot end + 2h; redeemed via the public reminder-login endpoint to mint a JWT (HS256, default 2h) + refresh token. |
| IIMT OTP / verified flags | src/controllers/iimt.controller.ts |
OTP send/verify | Redis keys iimt:otp:{number} (300s) and iimt:verified:{number} (1800s). |
ApplicationSubscriber.afterInsert |
src/subscribers/application.subscriber.ts |
Any application insert | Idempotently creates an ACTIVE Lead for the user (source my-analytics-school). |
UserSubscriber.afterInsert |
src/subscribers/user.subscriber.ts |
Any user insert | Idempotently creates a zero-balance Token. (Auto mentor-profile creation is present but commented out.) |
There are no Socket.IO events specific to this domain; the booked free slot reuses the meeting room machinery documented in the Mentorship & Meetings domain. There is no inbound webhook in this code path for application messages — ApplicationWhatsAppMessage delivery/read fields are updated elsewhere (status callback handling lives in the WhatsApp domain).
External integrations¶
| Integration | Used by | Env vars / config | Fallback / feature flag |
|---|---|---|---|
| Meta Cloud WhatsApp API | WhatsAppService.sendApplicationMessage (reminders), WhatsAppService.sendMessage (IIMT OTP) |
APPLICATION_MEETING_REMINDER_TEMPLATE_NAME (default career_session_15min_reminder), APPLICATION_MEETING_REMINDER_TEMPLATE_LANGUAGE (default en), APPLICATION_MEETING_REMINDER_PHONE_NUMBER_ID (default 1076667438862600), APPLICATION_MEETING_REMINDER_URL_PREFIX |
IIMT OTP send wrapped in try/catch — on failure the OTP is logged to the server console so flows can continue in dev. |
| Redis | OTP + verified flags + reminder login tokens | REDIS_HOST, REDIS_PORT |
— |
| JWT auth | reminder login token redemption | JWT_SECRET (required — throws if missing), APPLICATION_MEETING_REMINDER_JWT_TTL (default 2h) |
— |
| Email (Nodemailer via worker) | booking notifications | EMAIL_USER, EMAIL_PASS (see backend CLAUDE.md) |
Errors caught/logged. |
| Feature flag | application meetings on/off | ENABLE_APPLICATION_MEETINGS (default ON, src/utils/featureFlags.ts), APPLICATION_MEETINGS_EXPIRY_DATE (hard cutoff), MAX_FREE_MEETINGS_PER_STUDENT (default 3), APPLICATION_MEETING_DURATION_MINUTES (default 15), APPLICATION_MEETING_REMINDER_LEAD_MINUTES (default 15), APPLICATION_MEETING_TIME_ZONE (default Asia/Kolkata) |
When disabled or past expiry, booking/mentor endpoints return 403. |
| Razorpay | payment fields on Application |
(see Payments domain) | Payment is recorded onto the application row; the actual charge flow is owned by another domain. |
The IIMT campaign front-ends are explicitly allow-listed in CORS (src/app.ts: iimt-space-tech*.vercel.app).
Status lifecycles¶
stateDiagram-v2
[*] --> draft : wizard started / docs uploaded
draft --> submitted : POST /api/student/application
submitted --> under_review : admin picks up
under_review --> approved : admin approves
under_review --> rejected : admin rejects
approved --> payment_received : payment captured
draft --> paid : quick_pay before full submit
paid --> submitted : student completes wizard
payment_received --> batch_allocated : batch assigned
paid --> batch_allocated : batch assigned
batch_allocated --> enrolled : onboarding complete
approved --> enrolled : provisioned / fast-track
payment_received --> enrolled : provisioned / fast-track
enrolled --> deboarded : student leaves program
rejected --> [*]
deboarded --> [*]
note right of paid
Both draft and paid are
"editable" — uploads and
PATCH target these states.
end note
note right of enrolled
Provision Mentee writes
enrolled directly with all
milestone flags true.
end note
The enum defines the legal states; transitions above combine the explicit moves in code (
submitApplication→submitted;ApplicationService.updateApplicationStatusstampsreviewedAtonapproved/rejected;menteeProvision→enrolled). Some transitions (e.g.payment_received,batch_allocated) are driven by the Payments and LMS domains and are shown here for completeness (inferred from the enum and milestone flags).
Edge cases, limits & gotchas¶
- Active-application preference over newest draft. Uploading a document via
ApplicationService.updateApplicationDocuments(or the student upload endpoints) can spawn a strayDRAFT. The dashboard andgetApplicationdeliberately prefer an active application (enrolled/batch_allocated/paid/payment_received/approved) over a chronologically-newer draft, otherwise an enrolled student would be flipped back to the free/non-enrolled view. See comments instudent.controller.ts(~line 748) andApplicationService.updateApplicationDocuments. - Editable = draft or paid only.
submitApplication/updateApplicationreject edits for any other status with a 403 ("already submitted").getEditableApplicationByUserIdonly returnsdraft/paid. - One active application per batch.
submitApplicationreturns 409 if the user already has an active (submitted/under_review/approved/enrolled/batch_allocated/payment_received) application for a different batch. - Document
s3Keypreservation. Both submit and document-upload merge incoming docs over existing ones and re-attach the existings3Keywhen the incoming payload only carriesurl/name, so re-submitting does not drop the S3 reference. - Course override protection.
courseOverridden = truestops the batch-default auto-fill from clobbering a BL/Admin-assigned course on later batch changes (audited viaCourseAssignmentHistory). - Free-meeting guards. Booking enforces: feature enabled + not past
APPLICATION_MEETINGS_EXPIRY_DATE, mentor must be an application mentor, no active confirmed meeting, under the free cap, slot available & in the future, and exact duration match (default 15 min) — a mismatch is rejected with a precise message. - Idempotent reminder job id.
jobId = application-meeting-reminder-{slotId}means re-booking the same slot will not duplicate reminders (BullMQ dedupes by job id). - Reminder respects opt-out. Even after scheduling,
processReminderJobskips sending if the student turned off WhatsApp alerts or the slot is no longer confirmed. - Reminder login token security. Token redemption re-validates that the slot belongs to the user and the user is active before minting a JWT; the JWT carries
reminder: true+meetingIdso it is scoped to the meeting. - IIMT verification is Redis-only. If Redis loses the
iimt:verifiedkey (e.g. >30 min between verify and apply),applyreturns 403 and the visitor must re-verify. There is no persisted "verified attempt" outside the saved application. - IIMT OTP fallback leaks to logs. On WhatsApp send failure the OTP is printed to the server console (dev convenience) — acceptable for a low-stakes campaign but worth noting for production log hygiene.
- Admin user lookup for IIMT messages. The IIMT controller attaches outbound messages to the first
adminuser, falling back to the all-zero UUID if none is found — a missing admin will produce a placeholderuserId. - Provision Mentee is destructive on existing users. Converting an existing email overwrites the password and forces
role = user,stage = 3, verified/active — it requiresconfirmRoleChange: true(the UI shows a popup) precisely because it can downgrade/convert another account. - Phone normalization. Provisioning normalizes phone to a 10-digit Indian number (strips a leading
91), and storesnullif it cannot. - Auto-sync schema. TypeORM auto-sync is ON; the unique
masRollNumberconstraint and indices are enforced at the DB level.
Related docs¶
- Sales CRM & Leads — the
Leadauto-created byApplicationSubscriberand lead-to-application conversion (source = 'lead_conversion'). - Mentorship & Meetings — the
Slots/meeting machinery reused for free application meetings, mentor profiles, and the meeting room. - Education / LMS & Courses — batches, courses, enrollment, and the enrolled-student dashboard that onboarding milestone flags gate.