Exact endpoint ↔ screen mapping. Base URL https://api.<domain>/api/v1, bearer
auth, envelope shapes per 00-shared/07_API_Conventions.md. All routes under
@Controller('crm') (crm.controller.ts:30), JWT-guarded (crm.controller.ts:29);
RBAC permission decorators are not applied yet (perms exist at
permissions.constants.ts:34-36; enforcement (planned), IMPLEMENTATION_PLAN.md §5.1).
| # | Method | Path | Screen | Params/Body | Perm | Source |
| 1 | GET | /crm/leads | S1 | page (1), limit (20), status (enum) | crm.read | crm.controller.ts:37-45 |
| 2 | POST | /crm/leads | S3 | CreateLeadDto | crm.lead.manage | crm.controller.ts:47-51 |
| 3 | GET | /crm/leads/:id | S2 | — | crm.read | crm.controller.ts:53-57 |
| 4 | PATCH | /crm/leads/:id | S3/S2 | UpdateLeadDto (partial) | crm.lead.manage | crm.controller.ts:59-63 |
| 5 | POST | /crm/leads/:id/follow-ups | S2 | CreateFollowUpDto | crm.lead.manage | crm.controller.ts:65-69 |
| 6 | POST | /crm/leads/:id/convert | S4 | — | crm.lead.manage | crm.controller.ts:71-75 |
| # | Method | Path | Screen | Params/Body | Perm | Source |
| 7 | GET | /crm/campaigns | S10 | page, limit | crm.read | crm.controller.ts:77-81 |
| 8 | POST | /crm/campaigns | S11 | CreateCampaignDto | crm.campaign.manage | crm.controller.ts:83-87 |
| # | Method | Path | Screen | Params/Body | Perm | Source |
| 9 | POST | /crm/admissions | S7 | CreateAdmissionDto | crm.lead.manage | crm.controller.ts:91-95 |
| 10 | GET | /crm/admissions/stats | S5 | — | crm.read | crm.controller.ts:97-101 |
| 11 | GET | /crm/admissions | S5 | page, limit, status | crm.read | crm.controller.ts:103-111 |
| 12 | GET | /crm/admissions/:id | S6 | — | crm.read | crm.controller.ts:113-117 |
| 13 | PATCH | /crm/admissions/:id | S6 | UpdateAdmissionDto (partial) | crm.lead.manage | crm.controller.ts:119-123 |
| 14 | POST | /crm/admissions/:id/documents | S6 | CreateAdmissionDocumentDto | crm.lead.manage | crm.controller.ts:125-132 |
| 15 | POST | /crm/admissions/:id/schedule-interview | S6/S8 | ScheduleInterviewDto | crm.lead.manage | crm.controller.ts:134-141 |
| 16 | POST | /crm/admissions/:id/decision | S6/S9 | AdmissionDecisionDto | crm.lead.manage | crm.controller.ts:143-147 |
| 17 | POST | /crm/admissions/:id/convert | S6 | — | crm.lead.manage | crm.controller.ts:149-153 |
- List endpoints (1, 7, 11): envelope
data: Lead[]/Campaign[]/Admission[] +
meta: {page, limit, totalItems, totalPages, hasNext, hasPrevious} via
buildPaginationMeta (crm.service.ts:75,79,191,193; admission.service.ts:68-72).
- Detail (3, 12) & mutations:
data = document, no meta.
- Stats (10):
data: {new?: 0, submitted?: n, ..., total, conversionRate: x.x}
(admission.service.ts:227-237); counts keyed by AdmissionStatus enum.
| List | Server sort (fixed) | Filters supported | Source |
| Leads | createdAt: -1 | status only | lead.repository.ts:24-36 (sort at :31) |
| Admissions | submittedAt: -1 | status only | admission.repository.ts:25-35 (sort at :29) |
| Campaigns | createdAt: -1 | none | campaign.repository.ts:21-31 (sort at :25) |
No q, no sort, no assignedTo/source query params anywhere in CRM —
client-side filtering per 04 §6.
| Rule | Code | Message (verbatim) | Source |
| Duplicate lead email | 409 | Lead with email "{email}" already exists. | crm.service.ts:42-47 |
| Lead not found | 404 | Lead not found. | crm.service.ts:82-86,98 |
| Convert already-converted | 400 | Lead is already converted. | crm.service.ts:120-122 |
| Convert closed lead | 400 | Cannot convert a closed lead. | crm.service.ts:123-125 |
| Convert missing placement | 400 | Lead must have grade, academic year, and class assigned for conversion. | crm.service.ts:145-149 |
| Admission not found | 404 | Admission not found. | admission.service.ts:75-79 |
| Decide from wrong status | 400 | Admission in status "{status}" cannot be decided. | admission.service.ts:141-145 |
| Edit closed admission | 400 | Admission in status "{status}" is not editable. | admission.service.ts:257-269 |
| Convert non-approved | 400 | Only approved admissions can be converted. | admission.service.ts:174-178 |
| Convert missing placement | 400 | Admission must have grade, academic year, and class for conversion. | admission.service.ts:179-183 |
Cross-tenant access → 404 via scopedFilter (base.repository.ts:20-30).
Validation 400s carry details[{field, message}] (00-shared/07 §3).
| Event | Emitted at | Payload keys | Source |
LeadCreated | POST leads | leadId, firstName, lastName, email, source | crm.service.ts:50-63 |
LeadConverted | POST leads/:id/convert | leadId, studentId, userId | crm.service.ts:166-173 |
AdmissionSubmitted | POST admissions | admissionId, email | admission.service.ts:54-57 |
AdmissionApproved | decision approve | admissionId, email | admission.service.ts:158-162 |
AdmissionRejected | decision reject | admissionId, email | admission.service.ts:163-167 |
AdmissionConverted | POST admissions/:id/convert | admissionId, studentId | admission.service.ts:219-223 |
Waitlist decision emits no event today.
- Lead:
firstName, middleName?, lastName, email, phone?, source, status, gradeId?, sectionId?, academicYearId?, classId?, notes?, assignedTo?, followUps?: [{note, scheduledAt, completedAt?, createdBy?, createdAt}], convertedAt?, convertedToStudentId?, closedAt?, closedReason?, metadata? +
BaseSchema (tenantId, createdBy, updatedBy, isDeleted, deletedAt, deletedBy, version, createdAt, updatedAt) — lead.schema.ts:41-99; BaseSchema per
00-shared/01 §10.
- Admission:
firstName, middleName?, lastName, email, phone?, gradeId?, academicYearId?, classId?, status, documents?: [{type, fileId, filename, uploadedBy?, uploadedAt}], workflow?: [{from, to, approver?, comment?, at}], interview?: {scheduledAt, panel?, mode?, feedback?}, conversion?: {studentId, convertedAt}, notes?, submittedAt?, decidedAt? —
admission.schema.ts:91-144.
- Campaign:
name, description?, type, status, startDate?, endDate?, targetAudience?, metrics?: {leadsGenerated?, converted?, sent?, opened?, clicked?}, metadata? — campaign.schema.ts:23-56.
| Gap | Impact | Notes |
No lead search/q | can't find by name | global search incl. leads (planned) (IMPLEMENTATION_PLAN.md §3) |
No assignedTo/source list filters | "my leads" is client-side | server filters (planned) |
No nextContactDate field | follow-up due derived from followUps[] client-side | add field or dedicated endpoint (proposed) |
| No lead/admission DELETE | no hard/soft delete in UI | BaseRepository.softDelete exists (base.repository.ts:68-74); endpoint (planned) |
| No campaign detail/PATCH | campaigns read-only after create | (planned) |
| No campaign metrics write | metrics schema-only | (planned) |
| No document download/upload endpoints | fileId opaque | storage wiring (planned) |
No admission assignedTo | no owner for applications | (proposed) |
| Admission workers (reminder/expiry) | stale apps not auto-handled | queues named in 00-shared/01 §6; findStale ready (admission.repository.ts:53-62) |
| RBAC decorators | all routes = any authed user | enforcement (planned) (IMPLEMENTATION_PLAN.md §5.1) |
| Lead → batch conversion (coaching) | school-only flow today | (planned) (IMPLEMENTATION_PLAN.md §6.6) |