Skip to content

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 applications row owned by the student. Implemented in src/controllers/student.controller.ts (submit/update/get/upload-documents) and src/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 in src/controllers/applicationMeeting.controller.ts, src/services/ApplicationMeetingService.ts, and src/services/ApplicationMeetingReminderService.ts.
  • IIMT application flow — a separate, public partner-campaign intake (the iimt-space-tech front end) gated by a WhatsApp OTP, persisting to its own iimt_applications table. Implemented in src/controllers/iimt.controller.ts and src/entities/IIMTApplication.entity.ts.
  • Mentee provisioning — an admin/CSV tool that creates (or converts) a verified user plus an ENROLLED application with every onboarding milestone flag flipped true, landing the person directly on the enrolled dashboard. Implemented in src/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 DRAFT or PAID row; 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 the applications row, not from users.
  • Application mentor — an EXPERT user whose Mentor.isApplicationMentor flag is true, eligible to host free application meetings.
  • Free application slot — a Slots row with isFree = 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 (default application_wizard).
  • ApplicationWhatsAppMessage.status: queued | submitted | sent | delivered | read | failed; direction: outbound | inbound.
  • Slots for application meetings always carry isFree = true, source = 'application', and status = CONFIRMED once 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) and mentorApplication.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.tsscheduleReminder; 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 (submitApplicationsubmitted; ApplicationService.updateApplicationStatus stamps reviewedAt on approved/rejected; menteeProvisionenrolled). 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 stray DRAFT. The dashboard and getApplication deliberately 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 in student.controller.ts (~line 748) and ApplicationService.updateApplicationDocuments.
  • Editable = draft or paid only. submitApplication/updateApplication reject edits for any other status with a 403 ("already submitted"). getEditableApplicationByUserId only returns draft/paid.
  • One active application per batch. submitApplication returns 409 if the user already has an active (submitted/under_review/approved/enrolled/batch_allocated/payment_received) application for a different batch.
  • Document s3Key preservation. Both submit and document-upload merge incoming docs over existing ones and re-attach the existing s3Key when the incoming payload only carries url/name, so re-submitting does not drop the S3 reference.
  • Course override protection. courseOverridden = true stops the batch-default auto-fill from clobbering a BL/Admin-assigned course on later batch changes (audited via CourseAssignmentHistory).
  • 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, processReminderJob skips 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 + meetingId so it is scoped to the meeting.
  • IIMT verification is Redis-only. If Redis loses the iimt:verified key (e.g. >30 min between verify and apply), apply returns 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 admin user, falling back to the all-zero UUID if none is found — a missing admin will produce a placeholder userId.
  • Provision Mentee is destructive on existing users. Converting an existing email overwrites the password and forces role = user, stage = 3, verified/active — it requires confirmRoleChange: 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 stores null if it cannot.
  • Auto-sync schema. TypeORM auto-sync is ON; the unique masRollNumber constraint and indices are enforced at the DB level.

  • Sales CRM & Leads — the Lead auto-created by ApplicationSubscriber and 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.