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:
SUPERADMINwas folded intoADMINon this branch (seeUserRolecomment); the code still defensively double-checksADMINin several role guards (a leftover of that merge — the two branches ofrole === ADMIN || role === ADMINare identical).
Key concepts & entities¶
Glossary
- Ownership pool — every raw lead carries
owning_sales_head_id. A lead "belongs" to a sales head's pool (ornull= company pool). This is the per-lead isolation key, distinct from who is assigned to work it. - Assignment vs. ownership —
assignedTo(telecaller) andassignedCounsellorare who works the lead;owning_sales_head_idis 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.salesHeadIdpoints at the head, plus the head themselves, plus floaters (salespeople with NO head — shared company resources visible to every head). SeegetTeamMemberIds. - Scope — the computed visibility envelope for a request:
all(admin),head(pool + team), orself(own leads). - Isolation flag — a
SystemConfigrow (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 istrue. - Approval gate — bulk-assigning leads to a telecaller assigns them in the DB immediately but raises a
pendingapproval 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.SalespersonTargetis 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.notify → QueueService.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 inSalesScopeService.SalesHeadProfileflags — 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 aroundRobinAssignmentjob 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 ignoresowning_sales_head_identirely (it would assign across pools). - Assignment ≠ ownership. Bulk-assign moves
assignedToimmediately even before approval; the approval gate only controls the email, not the DB state. A leftoverpendingapproval does not block reassignment. - Approval ownership is strict. Only the
owningSalesHeadIdcan approve/notify/dismiss (403 otherwise). Admins do not go through the gate —bulkAssignonly raises an approval when the caller is a head (headActorId), andlistAssignmentApprovalsreturns[]for admins. - Floaters are shared. Salespeople with
salesHeadId = NULLare 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_HEADwith noSalesHeadProfilerow resolves toFULL_SALES_HEAD_PERMISSIONS(belt-and-braces against a skipped seed). So enablingrequireHeadPagePermissionchanges nothing for pre-existing heads. - Page-permission gate is a no-op for non-heads.
requireHeadPagePermissionpassesADMIN/SALESstraight 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,
getActiveHeadScopereturnsnull, 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(androle === ADMIN || role === ADMIN) — a harmless artifact of the SUPERADMIN→ADMIN merge; both operands are the same enum value. - Bulk-assign history is cheap.
RawLeadService.bulkAssigninserts history rows withfromUserId = null(it does not read each lead's prior owner), unlike singleassignwhich records the realfromUserId. - Multi-platform. These endpoints are not
x-platformbranched; they operate on the sharedmas/mas_crmdata regardless of the platform header. - Batch Lead / CM dual-source resolution. Both
resolveLedBatches(BL) andgetAssignedStudents(CM) union the join table (BatchLeadAssignment/CommunityManagerAssignment) with the direct field onBatch(batchLeadId/cmId). Assigning via either path works; an admin can mix both. Deleting a user cascades aBatchLeadAssignmentdelete (seedatabase.worker). - CM notes default to pinned.
CMNote.isPinneddefaults totrue; the UI relies ontoggleNotePinto unpin. - Admissions target is a placeholder.
TargetType.ADMISSIONShas no achieved computation yet (no admissions table); the grid will show target withachieved = 0.