40227-vm/backend/docs/communications.md

80 lines
7.9 KiB
Markdown

# Communications Backend
## Purpose
The communications area exposes two product-focused flows instead of generic CRUD:
- Parent/guardian direct messages through `direct_messages`.
- Internal alert events through `communication_events`.
## Slice Files (by layer)
- Route: `src/routes/communications.ts` (thin wiring; `GET /events`, `POST /events`). Mounted at `/api/communications` behind the `authenticated` middleware in `src/index.ts`.
- Controller: `src/api/controllers/communications.controller.ts` (custom — `listEvents`, `createEvent`).
- Service (BLL): `src/services/communications.ts` (+ `src/services/communications.types.ts`). Contains validation, scope resolution, and DTO mappers.
- Repository (DAL): queries run through `db.communication_events` inside the service (no separate `db/api` file).
- Models: `src/db/models/communication_events.ts`.
- Shared used: `services/shared/access.ts`, `shared/constants/communications.ts` (event-type values and manager role list), `shared/constants/roles.ts` (`ROLE_NAMES`), `shared/constants/pagination.ts` (`resolvePagination`), `shared/errors/forbidden.ts`, `shared/errors/validation.ts`.
Direct messages:
- Route: `src/routes/direct_messages.ts`, mounted at `/api/direct_messages`, authenticated and gated by `READ_PARENT_COMM`.
- Controller: `src/api/controllers/direct_messages.controller.ts`.
- Service: `src/services/direct_messages.ts`.
- Model: `src/db/models/direct_messages.ts`.
## API
All routes require JWT authentication.
- `GET /api/communications/events` -> `200` `{ rows, count }`. Optional query `type`; supports `limit`/`page`.
- `POST /api/communications/events` -> `201` the created event DTO. Body is `req.body.data`.
- `PATCH /api/communications/events/:id` -> `200` the updated event DTO. Body is `req.body.data`.
- `DELETE /api/communications/events/:id` -> `204`. Soft-deletes a wrongly-created alert without creating a user-facing notification.
- `POST /api/communications/events/:id/cancel` -> `201` the cancellation notification DTO. Body is `req.body.data` with optional `reason`.
- `GET /api/direct_messages/contacts` -> `200` `{ rows }`, contacts available through a shared student.
- `GET /api/direct_messages/conversations` -> `200` `{ rows }`, one row per `otherUserId + studentId`.
- `GET /api/direct_messages/thread/:otherUserId?studentId=:studentId` -> `200` the isolated thread for that staff/guardian/student context; marks incoming messages in that context as read.
- `POST /api/direct_messages/send` -> `200` the created message. Body is `req.body.data` with `recipientId`, `body`, and `studentId`.
Event DTO fields: `id`, `title`, `date` (from `event_date`), `type` (from `event_type`), `targetLevel`, `roles`, `organizationId`, `schoolId`, `campusId`, `classId`, `canceledEventId`, `createdById`, `updatedById`, `createdAt`, `updatedAt`.
Direct-message contact/conversation rows include `conversationKey`, `userId`, `name`, `role`, `studentId`, and `studentName`. `conversationKey` is derived from `userId + studentId`; it is a client key, not a stored column.
## Access Rules
- All endpoints require an authenticated user. Tenant-scoped alerts require a tenant context; platform-scope alerts are allowed for global-access managers.
- `POST /events` additionally requires `MANAGE_INTERNAL_COMM`. `custom_permissions` can grant it and `custom_permissions_filter` can remove it for non-global users.
- `PATCH /events/:id`, `DELETE /events/:id`, and `POST /events/:id/cancel` are allowed for the original creator or a manager with `MANAGE_INTERNAL_COMM` in the alert's scope. Global admins can mutate tenant alerts from platform root, but they do not see other users' tenant alerts in the root list; they see them through tenant drill-down.
- Alert create accepts exact targets: `system`, `all`, `organization`, `school`, and `campus`. Class-level create/targeting is intentionally not supported; class-scope users read campus-level alerts.
- Global root managers can create `system`, `all`, and selected organization/school/campus alerts. Organization managers can target their own organization, schools, or campuses. School managers can target their own school or campuses. Campus managers can target their own campus only.
- Direct messages require `READ_PARENT_COMM`. The granted audience is `office_manager`, `teacher`, and `guardian`.
- Direct-message access is membership-based and student-context based:
- Guardians can find the teacher and office manager connected to each linked student.
- Teachers can find guardians for students in their class.
- Office managers can find guardians for students on their campus.
- Threads and sends are allowed only when the requested `studentId` matches one of those contact rows.
- The frontend does not send organization, campus, creator, or updater fields. The backend fills them from the authenticated user.
## Tenant Scope
- Internal alerts are stored with an exact target. `targetLevel = system` with a null tenant chain is visible only in the platform/system scope. `targetLevel = all` with a null tenant chain is a platform-wide broadcast.
- List visibility includes the current scope and descendant targets. Platform-root users see platform alerts plus tenant-target alerts they created. When they drill into a tenant, they see that tenant scope like a scoped viewer. Organization users see their organization alerts plus school/campus alerts inside that organization. School users see their school alerts plus campus alerts inside that school. Campus/class users see their campus alerts only.
- Parent alerts do not automatically propagate down: an organization alert is not a campus alert unless the sender also targets that campus. Class-scope users read campus-level alerts because class content is campus-level.
- Selecting multiple tenant audiences creates multiple `communication_events` rows with the same title/date/type and different exact target stamps.
- Direct messages are not tenant-broadcast records. Contacts and threads are resolved through the current user's student links, class, or campus.
## Data Contract
- Event input (`EventInput`): `title` (required), `date` (required, stored to `event_date`), `type` (required; must be one of `meeting`, `drill`, `event`, `deadline`), `targets` (optional array of `{ level, id }`; omitted targets default to the creator's exact scope), `roles` (legacy metadata; it is not used for visibility).
- `communication_events` model: `title` (text), `event_date` (DATEONLY), `event_type` (ENUM of the four types), `targetLevel` (text: `system`, `all`, `organization`, `school`, `campus`), `roles` (JSONB, default `[]`), nullable `organizationId`, nullable `schoolId`, nullable `campusId`, nullable `classId`, nullable `canceledEventId`, `createdById` (not null), nullable `updatedById`, `paranoid` soft deletes, `belongsTo` associations to `organizations`, `campuses`, `users` (createdBy, updatedBy).
- List pagination: both lists use `resolvePagination(limit, page)`.
## Behavior / Notes
- Direct-message conversations are separated by student. The same guardian and teacher can have separate threads for two different students because reads/writes filter by `sender/recipient pair + studentId`.
- Event list orders by `event_date asc`, then `createdAt desc`. `createEvent` creates one row per exact target. `cancelEvent` creates a new cancellation notification with the same target stamp and soft-deletes the original alert; `deleteEvent` only soft-deletes the original alert.
- Validation failures throw `ValidationError`; access failures throw `ForbiddenError`.
## Tests
- `src/services/direct_messages.test.ts` covers contact discovery through linked
students, guardian/staff contact visibility, student-separated conversations,
ambiguous same-counterpart thread rejection, and persisted `studentId`
context when sending.
## Related
- Frontend: `frontend/docs/communications-integration.md`.
- Related backend slice: direct messages (`src/services/direct_messages.ts`) backs the guardian/staff Messages UI.