Skip to content

Sales CRM — Assignment, Targets, Sales Head & Community Managers

This document is the canonical reference for the operational backbone of the MAS Sales CRM and post-sale student ops: how leads get assigned to telecallers and counsellors, the sales-head approval gate over bulk assignment, the per-salesperson daily target sheet, the role-based visibility scope model (who sees which leads), sales automation workflows, and the two post-sale ownership roles — Batch Lead and Community Manager — who own a cohort and its students. It deliberately does not re-document raw-lead CRUD, call logging, follow-ups, telephony (Exotel), or WhatsApp — those belong to the lead-management doc; here we focus on assignment, approval, targets, scope and ownership.

Status: documented from source on this branch.


Overview

The Sales CRM spans the full funnel from a freshly-imported lead to an enrolled student handed off to a cohort owner. Several roles collaborate over shared data:

Persona Role enum (src/types/UserTypes.ts) What they own here
Admin ADMIN Bypasses everything. Administers ownership pools, sales-head profiles, batch-lead / CM assignment, workflow automation, lead transfers.
Sales Head SALES_HEAD Owns a lead pool + a team of salespeople. Bulk-assigns leads (behind an approval gate), sets targets, sees only their pool/team data (when isolation is on). Per-page access gated by profile flags.
Salesperson / Telecaller / Counsellor SALES Works their own assigned leads (/sales/crm/*). Auto-scoped to themselves.
Batch Lead BATCH_LEAD Owns one or more batches; manages onboarding, attendance, warnings, placement, roadmap for the students in their batches (/api/batchlead/*).
Community Manager COMMUNITY_MANAGER Assigned to students (or whole batches); records attendance, warnings, and pinned notes (/api/cm/*).

Where it sits in the suite: this domain reads raw_leads (CRM, mas_crm schema), users (public), Batch/Application (LMS), and emits emails via the BullMQ email queue. Frontends are the mas-website-live Sales/CRM panels and the mr-mentor-frontend admin dashboard.

Multi-product note: SUPERADMIN was folded into ADMIN on this branch (see UserRole comment); the code still defensively double-checks ADMIN in several role guards (a leftover of that merge — the two branches of role === ADMIN || role === ADMIN are identical).


Key concepts & entities

Glossary

  • Ownership pool — every raw lead carries owning_sales_head_id. A lead "belongs" to a sales head's pool (or null = company pool). This is the per-lead isolation key, distinct from who is assigned to work it.
  • Assignment vs. ownershipassignedTo (telecaller) and assignedCounsellor are who works the lead; owning_sales_head_id is whose pool it sits in. A lead can be reassigned within a pool without changing ownership.
  • Team / floaters — a head's "team" = salespeople whose users.salesHeadId points at the head, plus the head themselves, plus floaters (salespeople with NO head — shared company resources visible to every head). See getTeamMemberIds.
  • Scope — the computed visibility envelope for a request: all (admin), head (pool + team), or self (own leads).
  • Isolation flag — a SystemConfig row (sales_head_isolation) gating the read-side pool isolation rollout. Import-permission checks and ownership stamping are always on; read-filtering only kicks in when the flag is true.
  • Approval gate — bulk-assigning leads to a telecaller assigns them in the DB immediately but raises a pending approval the head must approve + "Notify" before the telecaller is emailed.
  • Target sheet — daily KPI targets per salesperson (calls, connected, webinar, admissions). Achieved counts are computed live from call logs / lead state; the table stores only targets.

Main TypeORM entities

Entity File Schema/table Purpose
LeadAssignmentHistory src/entities/LeadAssignmentHistory.ts mas_crm.lead_assignment_history Append-only audit of telecaller/counsellor handoffs.
LeadAssignmentApproval src/entities/LeadAssignmentApproval.ts mas_crm.lead_assignment_approvals One row per bulk-assign batch; drives the head's pending/approve/notify badge.
SalespersonTarget src/entities/SalespersonTarget.ts mas_crm.salesperson_targets Daily target per (user, date, type). Unique on those three.
SalesHeadProfile src/entities/SalesHeadProfile.ts mas_crm.sales_head_profiles Per-head permission flags (import sources + page access).
BatchLeadAssignment src/entities/BatchLeadAssignment.ts public.batch_lead_assignments Maps a batch lead → batch (join-table alternative to Batch.batchLeadId).
CommunityManagerAssignment src/entities/CommunityManagerAssignment.ts public.community_manager_assignments Maps a CM → student (optionally a batch).
CMNote src/entities/CMNote.ts public.cm_notes CM's notes on a student (meeting/call/general/warning/performance), pinnable.

Supporting entities referenced but owned elsewhere: RawLead (mas_crm.raw_leads), User (public.users, holds salesHeadId), Batch (holds batchLeadId + cmId), Application, LeadCallLog, VendorApiKey (holds assignedSalesHeadId), SystemConfig, and the workflow entities (Workflow, WorkflowEnrollment, WorkflowNodeRun).


Architecture

flowchart TD
  subgraph FE["Frontends"]
    SD["Sales dashboard / CRM (mas-website-live)"]
    AD["Admin dashboard (mr-mentor-frontend)"]
  end

  subgraph Routes["Express routes"]
    R1["/api/sales (sales.routes)"]
    R2["/api/admin/raw-leads + /sales/crm (rawLead.routes)"]
    R3["/api/admin/workflows (salesWorkflow.routes)"]
    R4["/api/batchlead (batchLead.routes)"]
    R5["/api/cm (cm.routes)"]
    R6["/api/admin/sales-heads (admin.routes)"]
  end

  subgraph MW["Middleware"]
    M1["authMiddleware -> req.user"]
    M2["requireHeadPagePermission (page flags)"]
    M3["batchLeadMiddleware / adminMiddleware"]
  end

  subgraph CTRL["Controllers"]
    C1["SalesController"]
    C2["RawLeadController"]
    C3["SalesWorkflowController"]
    C4["BatchLeadController"]
    C5["CMController"]
    C6["AdminController"]
  end

  subgraph SVC["Services"]
    S1["SalesDashboardService"]
    S2["RawLeadService"]
    S3["LeadAssignmentApprovalService"]
    S4["SalespersonTargetService"]
    S5["SalesCrmAnalyticsService"]
    S6["SalesScopeService"]
    S7["SalesHeadProfileService"]
    S8["WorkflowService + WorkflowEngineService"]
    S9["BatchLeadService"]
    S10["CMService"]
    Q["QueueService (BullMQ)"]
  end

  subgraph DB["PostgreSQL"]
    T1["raw_leads + lead_assignment_history"]
    T2["lead_assignment_approvals"]
    T3["salesperson_targets"]
    T4["sales_head_profiles"]
    T5["batch_lead_assignments / community_manager_assignments / cm_notes"]
    T6["lead_call_logs (achieved counts)"]
  end

  subgraph EXT["External / async"]
    E1["Email worker (telecaller digest)"]
    E2["leadAssignmentQueue (round-robin, DISABLED)"]
    E3["workflowQueue (5-min scan + steps)"]
  end

  SD --> R1 & R2 & R4 & R5
  AD --> R2 & R3 & R6
  R1 & R2 & R3 & R4 & R5 & R6 --> M1
  M1 --> M2 & M3
  M2 --> C1 & C2
  M3 --> C4 & C6
  C1 --> S1
  C2 --> S2 & S3 & S4 & S5 & S6
  C3 --> S8
  C4 --> S9
  C5 --> S10
  C6 --> S7
  S1 & S2 --> T1
  S3 --> T2
  S4 --> T3 & T6
  S5 --> T6 & T1
  S6 --> S7
  S7 --> T4
  S9 & S10 --> T5
  S3 --> Q --> E1
  Q --> E2 & E3

Data model

erDiagram
  USER ||--o{ RAW_LEAD : "owns pool (owning_sales_head_id)"
  USER ||--o{ RAW_LEAD : "assigned telecaller"
  USER ||--o{ RAW_LEAD : "assigned counsellor"
  USER ||--o{ LEAD_ASSIGNMENT_HISTORY : "from / to / changed_by"
  RAW_LEAD ||--o{ LEAD_ASSIGNMENT_HISTORY : "has handoffs"
  USER ||--o{ LEAD_ASSIGNMENT_APPROVAL : "owning sales head"
  USER ||--o{ LEAD_ASSIGNMENT_APPROVAL : "telecaller"
  USER ||--o{ SALESPERSON_TARGET : "has targets"
  USER ||--|| SALES_HEAD_PROFILE : "has profile"
  USER ||--o{ USER : "salesHeadId -> head"
  USER ||--o{ BATCH_LEAD_ASSIGNMENT : "batch lead"
  BATCH ||--o{ BATCH_LEAD_ASSIGNMENT : "assigned to"
  USER ||--o{ COMMUNITY_MANAGER_ASSIGNMENT : "community manager"
  USER ||--o{ COMMUNITY_MANAGER_ASSIGNMENT : "student"
  BATCH ||--o{ COMMUNITY_MANAGER_ASSIGNMENT : "optional batch"
  USER ||--o{ CM_NOTE : "cm + student"

  RAW_LEAD {
    uuid id PK
    string name
    string phone
    uuid assigned_to FK
    uuid assigned_counsellor FK
    uuid owning_sales_head_id FK
    string interest_level
    string webinar_status
    int call_count
  }
  LEAD_ASSIGNMENT_HISTORY {
    uuid id PK
    uuid lead_id FK
    string role
    uuid from_user_id FK
    uuid to_user_id FK
    uuid changed_by
    timestamp changed_at
  }
  LEAD_ASSIGNMENT_APPROVAL {
    uuid id PK
    uuid owning_sales_head_id FK
    uuid telecaller_id FK
    jsonb lead_ids
    int lead_count
    string status
    timestamp approved_at
    timestamp notified_at
  }
  SALESPERSON_TARGET {
    uuid id PK
    uuid user_id FK
    date date
    string type
    int target
    uuid set_by
  }
  SALES_HEAD_PROFILE {
    uuid id PK
    uuid user_id FK
    bool is_internal
    bool can_import_csv
    bool can_import_vendor
    bool can_import_website_campaign
    bool can_access_sales
    bool can_access_crm
    bool can_access_sales_tracking
    bool can_access_mas101_post_payment
  }
  BATCH_LEAD_ASSIGNMENT {
    uuid id PK
    uuid batchLeadId FK
    uuid batchId FK
    bool isActive
  }
  COMMUNITY_MANAGER_ASSIGNMENT {
    uuid id PK
    uuid communityManagerId FK
    uuid studentId FK
    uuid batchId FK
    bool isActive
  }
  CM_NOTE {
    uuid id PK
    uuid studentId FK
    uuid cmId FK
    string noteType
    bool isPinned
  }

Notable enums / status fields

  • LeadAssignmentApprovalStatus: pending | approved | notified | dismissed.
  • AssignmentRole (history): telly_caller | counsellor.
  • TargetType: calls | connected | webinar_registered | webinar_attended | admissions.
  • CMNote.noteType: meeting | call | general | warning | performance.
  • SalespersonTarget is unique on (user_id, date, type) — upserts are idempotent, last-write-wins for shared floaters.

API surface

Mount prefixes (from src/routes/index.ts): SalesController → /api/sales; RawLeadController & sales CRM → /api (so paths read /api/admin/raw-leads/... and /api/sales/crm/...); SalesWorkflow → /api; BatchLead → /api/batchlead; CM → /api/cm; sales-head profile admin → /api/admin.

Lead assignment, ownership & approval (src/routes/rawLead.routes.ts)

Method Path Auth/role Purpose
PATCH /api/admin/raw-leads/:id/assign admin + head; page sales/crm Assign/unassign one lead's telecaller; logs history.
PATCH /api/admin/raw-leads/bulk-assign admin + head; page sales/crm Bulk-assign telecaller; head path raises a pending approval.
GET /api/admin/raw-leads/assignment-approvals head (admins get []) List the head's actionable approvals.
GET /api/admin/raw-leads/assignment-approvals/count head Badge count of actionable approvals.
POST /api/admin/raw-leads/assignment-approvals/:id/approve owning head Mark approval approved.
POST /api/admin/raw-leads/assignment-approvals/:id/notify owning head Email telecaller digest, set notified.
POST /api/admin/raw-leads/assignment-approvals/:id/dismiss owning head Clear approval without notifying.
PATCH /api/admin/raw-leads/:id/counsellor admin + head; page crm Assign counsellor; logs history.
GET /api/admin/raw-leads/:id/history admin + head; page sales/crm Assignment-history timeline for a lead.
POST /api/admin/raw-leads/transfer-ownership admin only Move leads between ownership pools.
POST /api/admin/raw-leads/pool/preview · /pool/list · /pool/assign · /pool/tag admin only Lead-pool "Build & Assign": filter → preview → assign/tag.

Targets & CRM analytics (src/routes/rawLead.routes.ts)

Method Path Auth/role Purpose
POST /api/admin/sales-targets admin + head; page crm Upsert a daily target for a salesperson.
GET /api/admin/sales-targets admin + head; page crm Target/achieved grid + leaderboard.
GET /api/admin/sales-analytics/daily-calls admin + head; page crm Daily call pivot (caller × date × outcome).
GET /api/admin/sales-analytics/funnel admin + head; page crm Lifecycle funnel counts.

Sales dashboard (src/routes/sales.routes.ts, all behind authMiddleware)

Method Path Auth/role Purpose
GET /api/sales/dashboard · /stats · /kpis · /batch-stats sales-access check Dashboard aggregates.
GET /api/sales/sales-users head page sales_tracking/sales/crm Assignee dropdown (scoped to team).
GET /api/sales/sales-users/:id/summary head page sales_tracking Per-salesperson summary.
PATCH /api/sales/leads/:id/assign sales-access Assign a lead (team-scoped for heads).
PATCH /api/sales/applications/:id/assign sales-access Assign a (sales) application.

Sales automation workflows (src/routes/salesWorkflow.routes.ts, ADMIN only)

Method Path Auth/role Purpose
GET /api/admin/workflows admin List workflows + active enrollment counts.
POST /api/admin/workflows admin Create a workflow (validated graph).
GET/PUT/DELETE /api/admin/workflows/:id admin Read / update / delete a workflow.
PATCH /api/admin/workflows/:id/enabled admin Enable/disable (re-wakes paused enrollments).
POST /api/admin/workflows/:id/run-manual admin Hand-enroll picked leads.
GET /api/admin/workflows/:id/enrollments admin List enrollments.
GET /api/admin/workflows/:id/enrollments/:eid/runs admin Per-enrollment node runs.
POST /api/admin/workflows/:id/enrollments/:eid/retry · /retry-from admin Retry a failed enrollment (optionally from a node).
GET /api/admin/workflows/metadata admin Builder metadata (templates, tags, vendors, Aarya agents).
POST /api/admin/workflows/scan-now admin Enqueue an immediate trigger scan.

Sales-head profile administration (src/routes/admin.routes.ts)

Method Path Auth/role Purpose
GET /api/admin/sales-head admin List sales heads.
POST /api/admin/sales-head admin Create a sales head.
GET /api/admin/sales-heads/:userId/profile admin Read a head's effective permission flags.
PATCH /api/admin/sales-heads/:userId/profile admin Update a head's permission flags.
GET/POST /api/admin/batch-leads · /community-managers admin List/create batch leads & CMs.

Batch Lead (src/routes/batchLead.routes.ts, authMiddleware + batchLeadMiddleware)

Method Path Purpose
GET /api/batchlead/batches Batches this BL leads (join-table ∪ direct field).
GET /api/batchlead/dashboard · /batches/:batchId/stats Dashboard stats.
GET /api/batchlead/students · /students/:id Roster + student detail.
PATCH /api/batchlead/students/:id · /course · /diagnostic · /placement · /roll-number · /tokens Student field updates.
POST /api/batchlead/students/:id/warnings · /deboard · /reboard · /onboarding/complete Student ops.
GET/POST/PATCH /api/batchlead/meetings (+ /:id/status) Batch meetings.
GET /api/batchlead/risk-students (+ /recompute, /:id/review) Students-at-risk card.
GET/PATCH /api/batchlead/queries (+ /:queryId) Student help-centre queries routed to this BL.

Community Manager (src/routes/cm.routes.ts, authMiddleware)

Method Path Purpose
GET /api/cm/dashboard · /students · /students/:id Dashboard + assigned roster.
PATCH /api/cm/students/:id/onboarding Update onboarding milestones.
PATCH/GET /api/cm/students/:studentId/attendance · POST /api/cm/attendance/upload Attendance.
GET/POST /api/cm/warnings · /warnings/:studentId/issue · /warnings/:warningId/revoke Warnings.
GET/POST /api/cm/students/:studentId/notes · PATCH /api/cm/notes/:noteId/pin CM notes.
GET/PATCH /api/cm/batches · /my-courses · /batch-meetings Batches & meetings.

User journeys

Journey 1 — Manual single-lead assignment with scope + history

A head or admin assigns one lead to a telecaller. The service writes an audit row only when the assignee actually changes and emits a lead.assigned event that can trigger automation.

sequenceDiagram
  participant FE as CRM UI
  participant API as RawLeadController.assign
  participant Scope as SalesScopeService
  participant Svc as RawLeadService
  participant DB as Postgres
  participant Evt as LeadEventService

  FE->>API: PATCH assign with assignedTo
  API->>Scope: getActiveHeadScope for req.user
  Scope-->>API: head scope or null
  alt caller is a scoped head
    API->>Svc: assert lead is in head pool and assignee in team
    Svc-->>API: ok or 404 or 400
  end
  API->>Svc: assign leadId, assignedTo, changedBy
  Svc->>DB: load lead then set assignedTo and assignedAt
  alt previous owner differs from new
    Svc->>DB: insert lead_assignment_history row telly_caller
    Svc->>Evt: emit lead.assigned when assignee not null
  end
  Svc-->>API: updated lead
  API-->>FE: 200 with lead

Journey 2 — Sales-head BULK assignment with the approval gate

This is the headline assignment flow. The head selects many leads and a telecaller. Leads are assigned in the DB immediately, but a pending approval is raised; the telecaller is emailed only on the explicit Notify step. There is no daily cron — Notify is manual.

sequenceDiagram
  participant FE as Head CRM UI
  participant API as RawLeadController.bulkAssign
  participant Scope as SalesScopeService
  participant Svc as RawLeadService
  participant Appr as LeadAssignmentApprovalService
  participant DB as Postgres
  participant Q as Email queue

  FE->>API: PATCH bulk-assign with ids and assignedTo
  API->>Scope: headScope for caller
  alt scoped head
    API->>Svc: assert assignee in team and allInHeadScope ids
    Svc-->>API: ok or 404
  end
  API->>Svc: bulkAssign ids, assignedTo
  Svc->>DB: update raw_leads then insert history rows
  Svc-->>API: affected count
  alt caller is a head and assignedTo not null
    API->>Appr: createForBulkAssign with leadIds and telecaller and head
    Appr->>DB: insert lead_assignment_approvals status pending
    Appr-->>API: approval id
  end
  API-->>FE: 200 affected and approvalId

  Note over FE,Appr: Later the head clears the badge
  FE->>API: POST approvals approve
  API->>Appr: approve id, headId
  Appr->>DB: set status approved and approvedAt
  FE->>API: POST approvals notify
  API->>Appr: notify id, headId
  Appr->>DB: re-read leads snapshot and load telecaller
  Appr->>Q: addEmailJob telecaller-lead-assignment digest
  Appr->>DB: set status notified and notifiedAt
  Appr-->>API: notified approval
  API-->>FE: 200 done

Guards inside the approval service (LeadAssignmentApprovalService): an approval can only be acted on by its owningSalesHeadId (else 403); notify requires status approved first (else 400); already-notified or dismissed rows reject with 409; a telecaller with no email yields 400. The email body is rebuilt from the current lead rows but scoped to the snapshot leadIds, so the digest reflects up-to-date name/phone/tags even if some assignments later change.

Journey 3 — Sales head defines scope + sets daily targets

An admin grants a head their permission profile and assigns salespeople (users.salesHeadId). The head then opens the Targets page, which computes their team and renders the target/achieved grid.

sequenceDiagram
  participant Admin as Admin UI
  participant AC as AdminController
  participant Prof as SalesHeadProfileService
  participant Head as Head UI
  participant RC as RawLeadController
  participant Tgt as SalespersonTargetService
  participant DB as Postgres

  Admin->>AC: PATCH sales-heads profile flags
  AC->>Prof: upsertProfile userId, flags, actor
  Prof->>DB: validate role sales_head then save flags
  Prof-->>AC: saved flags
  AC-->>Admin: 200 flags

  Head->>RC: POST sales-targets userId date type target
  RC->>Tgt: upsert target keyed on user date type
  Tgt->>DB: insert or update salesperson_targets
  Tgt-->>RC: saved target
  RC-->>Head: 200 target

  Head->>RC: GET sales-targets from to type
  RC->>RC: getActiveHeadScope to find teamIds
  RC->>Tgt: grid and leaderboard scoped to teamIds
  Tgt->>DB: read targets then count achieved from lead_call_logs
  Tgt-->>RC: cells and leaderboard
  RC-->>Head: 200 grid

Achieved counts are computed live: calls and connected aggregate mas_crm.lead_call_logs (connected = outcome in connected/picked); webinar_registered/webinar_attended aggregate raw_leads.webinar_status attributed to COALESCE(assigned_counsellor, assigned_to); admissions is a placeholder until an admissions table exists.

Journey 4 — Round-robin auto-assignment worker (currently DISABLED)

A repeatable BullMQ job is scheduled every 15 minutes to distribute unassigned active leads evenly across active sales users. The worker import is commented out in src/index.ts, so jobs enqueue but nothing consumes them today.

sequenceDiagram
  participant Cron as BullMQ repeat every 15 min
  participant Q as leadAssignmentQueue
  participant W as leadAssignment.worker (DISABLED import)
  participant Svc as SalesDashboardService
  participant DB as Postgres

  Cron->>Q: enqueue roundRobinAssignment job
  Note over Q,W: worker not started so job sits unprocessed
  W->>Svc: getSalesUsers active
  Svc-->>W: sales users
  W->>Svc: getLeads status active limit 10000
  Svc-->>W: leads then filter unassigned
  loop each unassigned lead i
    W->>Svc: assignLead leadId to salesUsers i mod N
    Svc->>DB: set assignedTo
  end
  W-->>Q: result with assigned and failed counts

Journey 5 — Sales automation workflow triggers an action on a lead

Admins build a workflow graph (one trigger node + action nodes). A 5-minute scan finds matching leads, enrolls them, and the engine advances each enrollment node-by-node, performing actions like send-email, send-WhatsApp, add-tag, assign, Aarya call, and wait/delay.

sequenceDiagram
  participant Admin as Admin UI
  participant WC as SalesWorkflowController
  participant WS as WorkflowService
  participant Q as workflowQueue
  participant Eng as WorkflowEngineService
  participant DB as Postgres

  Admin->>WC: POST workflows with graph and trigger config
  WC->>WS: create validates single trigger node
  WS->>DB: save workflow enabled
  Note over Q,Eng: every 5 minutes a scan runs
  Q->>Eng: workflow scan job
  Eng->>DB: find leads matching trigger segment in head pool
  Eng->>DB: insert enrollments skipping already-active
  Eng->>Q: enqueue step job per enrollment
  Q->>Eng: step job for enrollment
  Eng->>DB: read current node then run action
  alt action is wait or delay
    Eng->>DB: set enrollment waiting until time
  else action completes
    Eng->>DB: record node run then advance to next node
    Eng->>Q: enqueue next step job
  end

Workflows can carry an owningSalesHeadId; the engine scopes matched leads to that pool. Disabling a workflow pauses its enrollments; re-enabling re-wakes paused enrollments and kicks a scan. run-manual hand-enrolls picked leads (admin routes pass null head scope; the workflow's own owningSalesHeadId still applies).

Journey 6 — Batch Lead assignment to a cohort & student management

An admin makes a batch lead own a batch via either the Batch.batchLeadId field or a BatchLeadAssignment join row. The BL then sees a union of both sources and operates only on students in those batches.

sequenceDiagram
  participant Admin as Admin UI
  participant BS as BatchService
  participant DB as Postgres
  participant BL as Batch Lead UI
  participant BLC as BatchLeadController
  participant BLS as BatchLeadService

  Admin->>BS: update batch set batchLeadId
  BS->>DB: validate user role then save batch
  BL->>BLC: GET batchlead batches
  BLC->>BLS: resolveLedBatches batchLeadId
  BLS->>DB: read active batch_lead_assignments
  BLS->>DB: read batches where batchLeadId direct
  BLS-->>BLC: merged unique batches
  BLC-->>BL: 200 batches
  BL->>BLC: PATCH student placement or warning or onboarding
  BLC->>BLS: verifyStudentAccessForBatchLead then mutate
  BLS->>DB: assert student in a led batch then update
  BLS-->>BLC: result or 403 out of scope
  BLC-->>BL: 200

Journey 7 — Community Manager assignment + note on a student

A CM is linked to students (per-student CommunityManagerAssignment, or implicitly through a batch's cmId). The CM dashboard resolves the union of both, then the CM records attendance, warnings, and pinned notes.

sequenceDiagram
  participant CM as CM UI
  participant CMC as CMController
  participant CMS as CMService
  participant DB as Postgres

  CM->>CMC: GET cm students
  CMC->>CMS: getAssignedStudents cmId
  CMS->>DB: read active community_manager_assignments
  CMS->>DB: read batches where cmId then applications for those batches
  CMS-->>CMC: union of student ids resolved to roster
  CMC-->>CM: 200 students
  CM->>CMC: POST cm students notes with text and type
  CMC->>CMS: saveNote cmId, studentId, text, type
  CMS->>DB: insert cm_notes row pinned by default
  CMS-->>CMC: saved note
  CMC-->>CM: 200 note
  CM->>CMC: POST cm warnings issue
  CMC->>CMS: issueWarning cmId, studentId
  CMS->>DB: insert warning then bump student warning state
  CMS-->>CMC: warning
  CMC-->>CM: 200

Background jobs & async

Queue Worker file Schedule Status Purpose
leadAssignmentQueue src/workers/leadAssignment.worker.ts repeat every 15 min (scheduleLeadAutoAssignment) scheduled but worker import is commented out in index.ts Round-robin distribute unassigned active leads. Jobs enqueue but go unprocessed today.
emailQueue src/workers/email.worker.ts on demand active Sends the telecaller-lead-assignment digest on Notify.
workflowQueue src/workers/workflow.worker.ts trigger-scan every 5 min (scheduleWorkflowScan) + per-step jobs active Enrolls matching leads and advances workflow nodes.

Notes: - Both lead-assignment and KPI/cleanup queues self-clean (removeOnComplete/removeOnFail capped at 50–100). - The approval Notify path is the only place this domain enqueues email — there is no recurring digest. - LeadEventService emits in-process events (lead.assigned, lead.counsellor_assigned) that workflow triggers can subscribe to (event-driven enrollment in addition to the 5-minute scan).

No Socket.IO events and no inbound webhooks are owned by this domain (telephony/WhatsApp webhooks live in the lead-management/Exotel surface).


External integrations

Integration Where Env / config Failure behavior
Email (Nodemailer via BullMQ) LeadAssignmentApprovalService.notifyQueueService.addEmailJob EMAIL_USER, EMAIL_PASS Telecaller digest is queued; if the telecaller has no email the call 400s before queueing.
CRM deep-link in email LeadAssignmentApprovalService MAS_WEBSITE_URL (default https://www.myanalyticsschool.com) Falls back to the default host; link is /sales/dashboard.
Vendor pools SalesScopeService.assertVendorsAssignedToHead reads VendorApiKey.assignedSalesHeadId A head importing "from vendor" may only pull from vendors assigned to them (else 403).
WhatsApp / Aarya (ElevenLabs) metadata WorkflowService.getMetadata Meta Cloud + ElevenLabs config Best-effort: returns [] if unconfigured so the workflow builder still loads.

Feature flags / config

  • sales_head_isolation (SystemConfig) — gates read-side pool isolation (Phase 3). When off, heads still get full read access (page permissions and ownership stamping remain on). Cached 30s in SalesScopeService.
  • SalesHeadProfile flags — per-head import sources (canImportCsv/canImportVendor/canImportWebsiteCampaign) and page access (canAccessSales/canAccessCrm/canAccessSalesTracking/canAccessMas101PostPayment).

Status lifecycles

Lead assignment approval

stateDiagram-v2
  [*] --> pending : head bulk-assigns to telecaller
  pending --> approved : head approves
  pending --> dismissed : head dismisses
  approved --> notified : head clicks Notify and email queued
  approved --> dismissed : head dismisses
  notified --> [*]
  dismissed --> [*]
  note right of notified
    terminal. re-notify rejected 409
  end note
  note right of approved
    notify requires approved first else 400
  end note

Community Manager / Batch Lead assignment (active flag)

stateDiagram-v2
  [*] --> active : assignment created isActive true
  active --> inactive : reassigned or deassigned isActive false
  inactive --> active : re-assigned
  note right of active
    only active rows count in roster resolution
  end note

Salesperson target (no explicit status — value lifecycle)

stateDiagram-v2
  [*] --> unset : no row for user date type
  unset --> set : upsert inserts row
  set --> set : upsert overwrites target last write wins
  note right of set
    achieved computed live, never stored
  end note

Edge cases, limits & gotchas

  • Auto-assignment worker is disabled. scheduleLeadAutoAssignment() still runs at startup (index.ts ~line 197), enqueuing a roundRobinAssignment job every 15 minutes, but the worker import (./workers/leadAssignment.worker) is commented out, so jobs accumulate unprocessed. Re-enable by uncommenting the import — but note the worker fetches up to 10,000 leads and assigns one-by-one (no batching), and ignores owning_sales_head_id entirely (it would assign across pools).
  • Assignment ≠ ownership. Bulk-assign moves assignedTo immediately even before approval; the approval gate only controls the email, not the DB state. A leftover pending approval does not block reassignment.
  • Approval ownership is strict. Only the owningSalesHeadId can approve/notify/dismiss (403 otherwise). Admins do not go through the gate — bulkAssign only raises an approval when the caller is a head (headActorId), and listAssignmentApprovals returns [] for admins.
  • Floaters are shared. Salespeople with salesHeadId = NULL are visible/assignable to every head (getTeamMemberIds). Their per-person aggregates (calls, targets) are shared across heads, and target rows are unique per (user, date, type) — so two heads setting a floater's target will clobber each other; floater targets are best set by admins.
  • Grandfathered heads = full access. A SALES_HEAD with no SalesHeadProfile row resolves to FULL_SALES_HEAD_PERMISSIONS (belt-and-braces against a skipped seed). So enabling requireHeadPagePermission changes nothing for pre-existing heads.
  • Page-permission gate is a no-op for non-heads. requireHeadPagePermission passes ADMIN/SALES straight through; the route's own role guard decides their access. It accepts multiple pages and grants if the head holds any (because one endpoint, e.g. the raw-leads list, backs both the Sales tab and the CRM page).
  • Isolation flag is read-side only. With the flag off, getActiveHeadScope returns null, so list/aggregate endpoints are not pool-filtered for heads. Import permission and ownership stamping are always enforced regardless.
  • Redundant role checks. Several middlewares contain role !== ADMIN && role !== ADMIN (and role === ADMIN || role === ADMIN) — a harmless artifact of the SUPERADMIN→ADMIN merge; both operands are the same enum value.
  • Bulk-assign history is cheap. RawLeadService.bulkAssign inserts history rows with fromUserId = null (it does not read each lead's prior owner), unlike single assign which records the real fromUserId.
  • Multi-platform. These endpoints are not x-platform branched; they operate on the shared mas/mas_crm data regardless of the platform header.
  • Batch Lead / CM dual-source resolution. Both resolveLedBatches (BL) and getAssignedStudents (CM) union the join table (BatchLeadAssignment / CommunityManagerAssignment) with the direct field on Batch (batchLeadId / cmId). Assigning via either path works; an admin can mix both. Deleting a user cascades a BatchLeadAssignment delete (see database.worker).
  • CM notes default to pinned. CMNote.isPinned defaults to true; the UI relies on toggleNotePin to unpin.
  • Admissions target is a placeholder. TargetType.ADMISSIONS has no achieved computation yet (no admissions table); the grid will show target with achieved = 0.