# 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.