01 — Product Overview (Staff Module)
- 1. Purpose
- 2. Module boundary: Staff vs Teachers vs Users
- 3. Staff lifecycle (end-to-end)
- 4. Department & designation catalog lifecycle
- 5. Role & permission mapping
- 6. Events & downstream consumers
- 7. Data model summary
- 8. Dependencies
- 9. Edge cases & invariants
- 10. PRD & mobile-forward note
StudyLyon — multi-tenant ERP / School Management API. This package designs the Staff module client (Flutter, forward-looking spec) against the implemented NestJS backend. Every endpoint, field, enum, event, and rule below is derived from
src/modules/staff/**,src/modules/rbac/**,src/database/**,src/infrastructure/bullmq/event-queue-map.ts, and the blueprint (studylyon-blueprint/04-Modules/Staff.md,03-Database/COLLECTIONS.md). Nothing is invented; gaps live in Assumptions & Open Questions and are marked(planned)/(proposed).
1. Purpose
The Staff module is the non-teaching employment domain: "who works the office, finance desk, front desk, library, transport, HR" of an institution. It owns:
| Responsibility | Source |
|---|---|
| Staff profile CRUD (create / list / detail / update / deactivate) | staff.controller.ts:24-38 |
| Duplicate guards (employeeNumber) on create | staff.service.ts:31-36 |
Soft-delete (deactivate) with isDeleted flag — no re-activation endpoint | base.repository.ts:68-74; staff.service.ts:93-104 |
Employment type enum (full_time/part_time/contract/intern) | staff.schema.ts:14-19 |
Staff status enum (active/inactive/on_leave/terminated) | staff.schema.ts:7-12 |
| Department catalog CRUD (name, code, head) | department.controller.ts:26-43; department.schema.ts:9-19 |
| Designation catalog CRUD (name, level, optional department link) | designation.controller.ts:26-43; designation.schema.ts:9-19 |
| Domain events → in-app notification, audit-write, search index | staff.service.ts:44-54,82-90,96-103; event-queue-map.ts:34-36; search-indexer.service.ts:11,18,25 |
Blueprint definition: Staff.md:3 — "Non-teaching employees." Collections:
staff, departments, designations (Staff.md:7).
2. Module boundary: Staff vs Teachers vs Users
The backend splits "people" into parallel profile documents sharing the same employment skeleton but diverging on domain specifics:
| Aspect | Staff (staff) | Teacher (teachers) |
|---|---|---|
| Identity | "Non-teaching employees." (Staff.md:3) | "Teaching profile." (blueprint) |
| Employment fields | employmentType, salaryGrade (staff.schema.ts:35-47) | qualification, experienceYears (teacher.schema.ts:38-42) |
| Academic linkage | none | subjects[], classTeacherFor[] (teacher.schema.ts:44-48) |
| Status field | status — StaffStatus enum (staff.schema.ts:7-12,48-49) | employmentStatus — same 4 values (teacher.schema.ts:7-12,31-36) |
| Unique indexes | tenantId+employeeNumber, tenantId+userId (staff.schema.ts:57-58) | identical pattern (teacher.schema.ts:56-57) |
| CRUD surface | staff.controller.ts:24-38 | teacher.controller.ts:24-38 |
| Events | StaffCreated/Updated/Deleted | TeacherCreated/Updated/Deleted |
| Reference catalogs | departments/designations owned here (Staff.md:59) | shared, owned by Staff module |
Users (users collection) holds identity/credentials; a users record may own
multiple profiles — a single userId can back both a teacher and a staff record in
parallel (RELATIONSHIPS.md:28 — staff (0..1) via userId). The Staff screens render
only staff records; the Teachers screens render only teacher records.
Two catalogs owned by this module (Staff.md:59 — "Departments/designations are
reference catalogs owned by this module"):
- Departments (
departmentscollection): flat org units. Example seeds fromCOLLECTIONS.md:1520-1533: Administration, Science, Commerce, Accounts, HR, Library, Transport. Fields:name,code,headId(refStaff),status(department.schema.ts:9-19). - Designations (
designationscollection): job titles. Example seeds fromCOLLECTIONS.md:1562-1573: Principal, Vice Principal, Teacher, Librarian, Receptionist, Accountant. Fields:departmentId?(refDepartment),name,level(numeric rank),status(designation.schema.ts:9-19).
Designations optionally link to a department (designation.schema.ts:9-10) — the
link is a reference only, not enforced (see OQ-3).
3. Staff lifecycle (end-to-end)
User account exists (Users module; email welcome via UserRegistered → emails queue)
→ Staff profile created POST /api/v1/staff [StaffCreated → in-app 'staff-created']
→ Employment record maintained: dept/designation/type/salary grade
→ Profile edited PATCH /api/v1/staff/:id [StaffUpdated → audit-write]
→ Deactivated DELETE /api/v1/staff/:id [StaffDeleted → audit-write]
(soft-delete: isDeleted=true + deletedAt + deletedBy; all queries exclude)
- Create always sets
status: StaffStatus.ACTIVE(staff.service.ts:39) — the create DTO cannot set status. employmentTypedefaults tofull_timewhen omitted (staff.service.ts:40-41).- Deactivation is permanent from the client's perspective:
DELETEsoft-deletes (base.repository.ts:68-74) and no re-activation endpoint exists. - Plan test rows: PLAN.md:38 (3.3 "Invite staff member (non-teaching) → email sent"
via
POST /api/v1/staff→StaffCreated), PLAN.md:40 (3.5 "Deactivate staff member → soft-delete" →StaffDeletedevent, queries exclude).
4. Department & designation catalog lifecycle
Department: POST /api/v1/departments (name required, duplicate name → 409)
PATCH /api/v1/departments/:id (rename, recode, reassign head)
DELETE /api/v1/departments/:id (soft-delete; no member-count guard — OQ-4)
Designation: POST /api/v1/designations (name required, duplicate name → 409)
PATCH /api/v1/designations/:id
DELETE /api/v1/designations/:id (soft-delete)
Catalogs are status-flagged, not deleted from history: status defaults to
'active' on create (department.service.ts:28, designation.service.ts:31);
DELETE sets isDeleted so the row disappears from all lists
(base.repository.ts:20-30,68-74). No events are emitted by either service —
no DepartmentCreated/DesignationCreated events exist in the event map
(event-queue-map.ts has no department/designation entries; both services inject
only their repository, department.service.ts:19-20, designation.service.ts:22-23).
5. Role & permission mapping
Permissions exist in permissions.constants.ts:19-24:
| Permission | Intended scope | Notes |
|---|---|---|
staff.read | View staff list/detail | |
staff.create | Create staff profile | |
staff.update | Edit staff profile | |
staff.delete | Deactivate staff | |
department.manage | Manage department catalog | single perm covers CUD |
designation.manage | Manage designation catalog | single perm covers CUD |
Default role wiring (role.schema.ts):
| Role | Slug | Permissions relevant to Staff module |
|---|---|---|
| Organization Admin | org_admin | ALL_PERMISSIONS — includes all six above (role.schema.ts:17-24) |
| Staff | staff | ['student.read'] only — no staff module perms (role.schema.ts:33-40) |
| Teacher / Accountant / Parent / Student | — | none of the six (role.schema.ts:25-48) |
Consequences for the client:
- The default Staff role cannot open the Staff section; HR/Admin must grant
staff.*perms via a custom RBAC role (rbacmodule) before the section is usable. - Server enforcement gap: staff controllers declare only
@UseGuards(JwtAuthGuard)(staff.controller.ts:21); no@Permissions()decorators are applied, unlike webhooks/search/files/scheduler (webhooks.controller.ts:22,search.controller.ts:16). TheRbacGuardexists (rbac.guard.ts:14-52) and is wired for other modules. Until staff controllers adopt it, permission checks are client-enforced only (mirror ofpermissions.constants.ts); treat server-side RBAC on these endpoints as(planned)— OQ-1.
6. Events & downstream consumers
| Event | Queue / job (event-queue-map.ts) | Consumer intent |
|---|---|---|
StaffCreated | in-app / staff-created (line 34) | In-app notification fan-out |
StaffUpdated | audit-write / log-staff-updated (line 35) | Audit trail |
StaffDeleted | audit-write / log-staff-deleted (line 36) | Audit trail |
StaffCreated/Updated/Deleted | Search indexer ENTITY_EVENTS (search-indexer.service.ts:11,18,25) | Search index upsert/remove |
Payloads: StaffCreated = {staffId, employeeNumber} (staff.service.ts:50-53);
StaffUpdated = {staffId, changes: Object.keys(dto)} (line 88); StaffDeleted =
{staffId} (line 102).
Observed gap (flag): the search indexer resolves entity ids from
payload.entityId ?? payload._id ?? ... (search-indexer.service.ts:62-67) and
titles from payload.name/title/firstName/... (lines 68-75). Staff payloads carry
staffId (not entityId/_id) and employeeNumber (not name), so staff creates
exit early at search-indexer.service.ts:88 — staff records are not indexed.
Verified in OQ-6.
Email: the emails queue worker handles only UserRegistered and
PasswordResetRequested (email.worker.ts:25-42); StaffCreated logs "No handler
for email event" (line 41). PLAN.md:38's "email sent" is satisfied at user
invitation time (UserRegistered), not at staff-profile creation — the client must
not promise a staff-creation email.
7. Data model summary
All three collections extend BaseSchema (base.schema.ts:8-35): tenantId
(required), createdBy/updatedBy/deletedBy, isDeleted (soft-delete, default
false), deletedAt, version (optimistic lock, $inc on every write —
base.repository.ts:63,71), createdAt, updatedAt. Every repository query injects
tenantId + isDeleted: false (base.repository.ts:20-30); platform admin bypasses
tenant scope but never sees deleted rows.
| Collection | Unique index | Ref fields |
|---|---|---|
staff | {tenantId, employeeNumber}; {tenantId, userId} (staff.schema.ts:57-58) | userId→User (required), departmentId→Department, designationId→Designation |
departments | {tenantId, name} (department.schema.ts:24) | headId→Staff |
designations | {tenantId, name} (designation.schema.ts:24) | departmentId→Department |
metadata is an open Object on staff (staff.schema.ts:51-52; update-staff.dto.ts:42)
— any key/value passthrough, unvalidated.
8. Dependencies
- Users — identity link (
userId,Staff.md:49). A staff record requires an existinguserId(create-staff.dto.ts:5-7). - RBAC — permission gating (
Staff.md:50). - Attendance / Leave / Payroll —
(planned)future consumers (Staff.md:51-52; salary grade is a reference, not payroll —Staff.md:60).
9. Edge cases & invariants
- Duplicate
employeeNumberwithin tenant → 409DUPLICATE_RESOURCE(staff.service.ts:32-36); duplicateuserIdwithin tenant → unique index violation → 409 (same code path viahttp-exception.filter.ts:32). - Duplicate department/designation
namewithin tenant → 409 (department.service.ts:23-25,designation.service.ts:25-28). - Unknown/invalid
:id→ 404RESOURCE_NOT_FOUND(staff.service.ts:60;http-exception.filter.ts:47-48,91-92maps CastError → 400). - Cross-tenant IDs: repository tenant-scoping makes them return 404, never leak
(
base.repository.ts:20-30). - Soft-deleted staff never reappear in lists; a second
DELETEon a deleted record → 404 (staff.service.ts:94-95). - Page/limit:
page≥ 1,limit1–100 default 20 (pagination-query.dto.ts:5-30).sort/qare accepted by the DTO but not applied by staff list queries (staff.service.ts:64-76— see OQ-2).
10. PRD & mobile-forward note
The PRD puts native mobile apps out of Phase 1 scope
(PRODUCT_REQUIREMENTS_DOCUMENT.md:144) — per the shared-package decision
(00-shared/01 §9, 00-shared/12 A1) these docs specify a full-featured Flutter
client to the complete API surface. For Staff specifically, mobile matters most for
HR on the go: quick lookups (employee number, department roster), status toggles,
and designation catalogs; heavy data-entry (bulk import) remains desktop/web. The
screen specs are responsive (phone/tablet/desktop) per 00-shared/02 §8.