Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

01 — Product Overview (Staff Module)

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:

ResponsibilitySource
Staff profile CRUD (create / list / detail / update / deactivate)staff.controller.ts:24-38
Duplicate guards (employeeNumber) on createstaff.service.ts:31-36
Soft-delete (deactivate) with isDeleted flag — no re-activation endpointbase.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 indexstaff.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:

AspectStaff (staff)Teacher (teachers)
Identity"Non-teaching employees." (Staff.md:3)"Teaching profile." (blueprint)
Employment fieldsemploymentType, salaryGrade (staff.schema.ts:35-47)qualification, experienceYears (teacher.schema.ts:38-42)
Academic linkagenonesubjects[], classTeacherFor[] (teacher.schema.ts:44-48)
Status fieldstatusStaffStatus enum (staff.schema.ts:7-12,48-49)employmentStatus — same 4 values (teacher.schema.ts:7-12,31-36)
Unique indexestenantId+employeeNumber, tenantId+userId (staff.schema.ts:57-58)identical pattern (teacher.schema.ts:56-57)
CRUD surfacestaff.controller.ts:24-38teacher.controller.ts:24-38
EventsStaffCreated/Updated/DeletedTeacherCreated/Updated/Deleted
Reference catalogsdepartments/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:28staff (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 (departments collection): flat org units. Example seeds from COLLECTIONS.md:1520-1533: Administration, Science, Commerce, Accounts, HR, Library, Transport. Fields: name, code, headId (ref Staff), status (department.schema.ts:9-19).
  • Designations (designations collection): job titles. Example seeds from COLLECTIONS.md:1562-1573: Principal, Vice Principal, Teacher, Librarian, Receptionist, Accountant. Fields: departmentId? (ref Department), 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.
  • employmentType defaults to full_time when omitted (staff.service.ts:40-41).
  • Deactivation is permanent from the client's perspective: DELETE soft-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/staffStaffCreated), PLAN.md:40 (3.5 "Deactivate staff member → soft-delete" → StaffDeleted event, 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:

PermissionIntended scopeNotes
staff.readView staff list/detail
staff.createCreate staff profile
staff.updateEdit staff profile
staff.deleteDeactivate staff
department.manageManage department catalogsingle perm covers CUD
designation.manageManage designation catalogsingle perm covers CUD

Default role wiring (role.schema.ts):

RoleSlugPermissions relevant to Staff module
Organization Adminorg_adminALL_PERMISSIONS — includes all six above (role.schema.ts:17-24)
Staffstaff['student.read'] only — no staff module perms (role.schema.ts:33-40)
Teacher / Accountant / Parent / Studentnone 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 (rbac module) 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). The RbacGuard exists (rbac.guard.ts:14-52) and is wired for other modules. Until staff controllers adopt it, permission checks are client-enforced only (mirror of permissions.constants.ts); treat server-side RBAC on these endpoints as (planned) — OQ-1.

6. Events & downstream consumers

EventQueue / job (event-queue-map.ts)Consumer intent
StaffCreatedin-app / staff-created (line 34)In-app notification fan-out
StaffUpdatedaudit-write / log-staff-updated (line 35)Audit trail
StaffDeletedaudit-write / log-staff-deleted (line 36)Audit trail
StaffCreated/Updated/DeletedSearch 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:88staff 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.

CollectionUnique indexRef 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 existing userId (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 employeeNumber within tenant → 409 DUPLICATE_RESOURCE (staff.service.ts:32-36); duplicate userId within tenant → unique index violation → 409 (same code path via http-exception.filter.ts:32).
  • Duplicate department/designation name within tenant → 409 (department.service.ts:23-25, designation.service.ts:25-28).
  • Unknown/invalid :id → 404 RESOURCE_NOT_FOUND (staff.service.ts:60; http-exception.filter.ts:47-48,91-92 maps 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 DELETE on a deleted record → 404 (staff.service.ts:94-95).
  • Page/limit: page ≥ 1, limit 1–100 default 20 (pagination-query.dto.ts:5-30). sort/q are 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.