14 — QA Checklist (CRM Module)
- 1. Duplicate lead detection
- 2. Status validation & transitions
- 3. Assignment & ownership
- 4. Soft-delete semantics
- 5. Follow-up integrity
- 6. Admissions documents / interview
- 7. List/pagination/sort contracts
- 8. Events & workers
- 9. Permissions & RBAC
- 10. Performance & offline
- 11. Accessibility (module additions to 00-shared/09 §12)
- 12. Localization & formatting
- 13. Performance budgets (module)
- 14. Release gates (module additions)
Module-specific checklist layered on 00-shared/10_QA_Baseline.md + 00-shared/09 accessibility baseline. Every item testable; backend rules cite source lines.
1. Duplicate lead detection
-
Same email twice → second POST returns 409
Lead with email "x" already exists.(crm.service.ts:42-47); no second lead doc. -
Email comparison is case-insensitive:
A@X.comvsa@x.comcollide (lead.schema.ts:52-53lowercase;lead.repository.ts:19lowercases query). -
Duplicate check is tenant-scoped (
scopedFilter,base.repository.ts:20-30): same email in two tenants → both allowed. - Create UI: 409 shows inline banner, form keeps values, no data loss, no duplicate double-submit (00-shared/08 §6).
-
PATCH changing email to an existing one → 409? — known gap: update path
does not check duplicates (
crm.service.ts:88-100only merges fields); flag + track.
2. Status validation & transitions
-
Status accepts only
new|contacted|qualified|converted|closed(lead.schema.ts:7-13); invalid value → 400VALIDATION_ERROR(@IsEnum,update-lead.dto.ts:42-45). -
Setting
status: closedwithoutclosedReason→closedAtauto-stamped (crm.service.ts:94-96); with reason →closedAtstill stamped, reason stored. -
Convert on
convertedlead → 400Lead is already converted.(crm.service.ts:120-122); onclosed→ 400Cannot convert a closed lead.(:123-125). -
Convert without gradeId+academicYearId+classId → 400 with exact placement
message (
crm.service.ts:145-149). -
Successful convert: status
converted,convertedAt,convertedToStudentIdset (crm.service.ts:161-164); one User + one Student doc created; no duplicate when user email already exists (reuse path:132-143). -
Admission decision only from decidable statuses
(
ADMISSION_DECIDABLE_STATUSES,admission.schema.ts:20-25); others → 400 (admission.service.ts:141-145). -
Decision maps: approve→
approved, reject→rejected, waitlist→waitlisted;decidedAtset; workflow stage appended with approver + comment (admission.service.ts:147-155,239-255). -
Convert admission only from
approved→ else 400 (admission.service.ts:174-178).
3. Assignment & ownership
-
PATCH
assignedToaccepts MongoId (update-lead.dto.ts:72-75); invalid → 400. -
List filter by
assignedTodoes not exist server-side (onlystatus—crm.controller.ts:37-45): "my leads" is client-side; verify no misleading server claims (flag as gap). -
assignedToisStaffref (lead.schema.ts:79-80); display names resolve from staff module (cross-module read(planned)). -
Follow-up
createdByis stamped from JWT user, never from body (crm.service.ts:111) — client cannot spoof.
4. Soft-delete semantics
- No delete endpoints exist in CRM — UI must not offer delete; verify 405 on any client-invented DELETE.
-
BaseRepository.softDelete(base.repository.ts:68-74) exists but is unreachable from CRM controllers — when(planned)delete lands: deleted leads excluded from lists (isDeleted: falsein scoped filter,base.repository.ts:20-30), version incremented. - Cross-tenant read of another tenant's lead/admission id → 404, not 403 or 500 (no existence leak).
-
Closed leads remain visible under status filter (never auto-purged);
metadatasurvives all PATCHes.
5. Follow-up integrity
-
POST follow-up with invalid
scheduledAt→ 400 (@IsDateString,create-follow-up.dto.ts:9-11). -
Follow-ups append (never replace): two POSTs → array length 2
(
crm.service.ts:114). -
completedAtoptional; uncompleted follow-ups drive the derived "next follow-up due" (client logic — unit testfollowUpSummary). -
Timestamps round-trip ISO-8601 in tenant timezone (00-shared/10 §3 pattern);
overdue detection uses tenant-local
now.
6. Admissions documents / interview
-
Document type enum:
tc|marksheet|certificate|photo|other(admission.schema.ts:27-33); invalid → 400. -
filenamedefaults tofileIdserver-side when absent (admission.service.ts:107). -
First document on
submittedadmission → status auto-advances todocuments_pending(admission.service.ts:112-114); on other open statuses → status unchanged. -
Scheduling interview sets
interview{...}+ statusinterview_scheduledwhen different (admission.service.ts:124-132); re-schedule overwrites cleanly. -
Interview
moderestrictedonline|offline(schedule-interview.dto.ts:22-25; defaultoffline). -
Document/interview/edit on closed admission → 400
not editable(admission.service.ts:257-269); UI locks simultaneously.
7. List/pagination/sort contracts
-
Leads default sort
createdAt: -1(lead.repository.ts:31); admissionssubmittedAt: -1(admission.repository.ts:29); campaignscreatedAt: -1(campaign.repository.ts:25) — verify row order client-side. -
limitbounds (default 20; 1–100 per 00-shared/07 §5):limit=200behavior verified (server default cap). -
meta={page, limit, totalItems, totalPages, hasNext, hasPrevious}(buildPaginationMeta); infinite scroll stops athasNext=false. -
Invalid
statusquery value → 400 (typedLeadStatus/AdmissionStatusparams,crm.controller.ts:42,108). -
Stats endpoint sums match list counts per status (
admission.service.ts:227-237);conversionRate= converted/total × 100, 1 decimal,0when empty.
8. Events & workers
-
Create lead emits
LeadCreatedwith leadId/name/email/source (crm.service.ts:50-63); convert emitsLeadConvertedwith leadId/studentId/userId (:166-173). -
Admission emits
AdmissionSubmitted|Approved|Rejected|Convertedat the right transitions (admission.service.ts:54-57,158-168,219-223); waitlist emits none (documented behavior). -
Reminder/expiry workers
(planned)(IMPLEMENTATION_PLAN.md §1.3): when landed, verifyfindStaleglobal scan (admission.repository.ts:53-62) excludes soft-deleted + respects cutoff; no cross-tenant action without tenantId in event.
9. Permissions & RBAC
-
Permission strings
crm.read,crm.lead.manage,crm.campaign.manage(permissions.constants.ts:34-36) referenced by UI gates. -
Known gap: endpoints are
JwtAuthGuard-only (crm.controller.ts:29) — any authenticated user can write today; assert current behavior and track RBAC enforcement(planned)(IMPLEMENTATION_PLAN.md §5.1). -
Client hides write actions without
crm.lead.manage/crm.campaign.manage(defense-in-depth until server enforces). -
admissions/statscorrectness: no per-status leak beyond tenant scope.
10. Performance & offline
-
Leads list (20 rows) first frame from cache < 300 ms;
ListView.buildermandatory (00-shared/11 §13). - S5 stats + list parallel fetch: stats failure never blocks list.
-
Offline: cached list +
AppOfflineBanner; all CRM writes blocked offline with clear message (no offline queue by design, 13 §12). - Large follow-up arrays: timeline uses builder, no layout jank.
- Detail fetch per navigation (no stale cache) — verify no blank flash (skeleton shown).
11. Accessibility (module additions to 00-shared/09 §12)
- Status badges announce icon + label (no color-only: converted/closed, approved/rejected) (00-shared/09:76).
- Timeline (follow-ups, workflow) fully readable via TalkBack/VoiceOver, per-stage semantics + dates.
- Convert/decision dialogs: warnings read before focus lands on confirm.
- Text scale 2×: filter chip rows scroll, no clipped status text.
- Keyboard-only: chip arrows + Enter; Esc closes sheets/dialogs; focus returns to invoker.
12. Localization & formatting
-
All
crm.*i18n keys present in en + org locales; server business-4xx text mapped via error-code→key fallback (00-shared/11 §9). -
Dates via
Intltenant locale; wire formatYYYY-MM-DD/ISO-8601 independent of display; RTL-safe rows.
13. Performance budgets (module)
| Check | Budget |
|---|---|
| List first frame (cached) | < 300 ms |
| Status filter round-trip | < 500 ms p95 with skeleton kept |
| Detail open | < 500 ms to content (network) |
| Convert flow (server chain) | < 3 s with in-dialog progress |
| Admissions stats tile | < 1 s; failure → "—" |
14. Release gates (module additions)
-
e2e parity: leads CRUD + 409 duplicate + convert 400s + admission decision
400s pass against live backend (pattern:
p1-operations.e2e-spec.ts). -
Goldens:
LeadStatusChip×6,StatusBadge×9,WorkflowTimeline,LeadListTile,FunnelStatCard— light/dark × 3 sizes. - No pending P0: duplicate-email UX, terminal-state locks, cross-tenant 404.