40227-vm/backend/docs/communications.md
2026-06-12 06:55:35 +02:00

60 lines
6.3 KiB
Markdown

# Communications Backend
## Purpose
The communications slice exposes product-focused endpoints for parent messages and internal alert events, instead of the generated CRUD routes. Parent messages reuse the existing `messages` and `message_recipients` tables; internal alerts own the `communication_events` table. The backend is the source of truth for these records.
## Slice Files (by layer)
- Route: `src/routes/communications.ts` (thin wiring; `GET /parent-messages`, `POST /parent-messages`, `GET /events`, `POST /events`). Mounted at `/api/communications` behind the `authenticated` middleware in `src/index.ts`.
- Controller: `src/api/controllers/communications.controller.ts` (custom — `listParentMessages`, `createParentMessage`, `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.messages`, `db.message_recipients`, and `db.communication_events` inside the service (no separate `db/api` file).
- Models: `src/db/models/communication_events.ts`; plus the existing `src/db/models/messages.ts` and `src/db/models/message_recipients.ts` (used by the parent-message flow).
- Shared used: `db/with-transaction.ts`, `services/shared/access.ts`, `services/shared/validate.ts` (`nullableString`), `shared/object.ts` (`isRecord`), `shared/constants/communications.ts` (channels, audiences, statuses, recipient types, event-type values, parent-message categories, manager/tenant-wide role lists), `shared/constants/roles.ts` (`ROLE_NAMES`), `shared/constants/pagination.ts` (`resolvePagination`), `shared/errors/forbidden.ts`, `shared/errors/validation.ts`.
## API
All routes require JWT authentication.
- `GET /api/communications/parent-messages` -> `200` `{ rows, count }`. Optional query `category`; supports `limit`/`page`.
- `POST /api/communications/parent-messages` -> `201` the created parent-message DTO. Body is `req.body.data`.
- `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`.
Parent-message DTO fields: `id`, `text` (from `body`), `to` (first recipient `recipient_label`), `date` (ISO, from `sent_at` or `createdAt`), `category` (derived from `subject`), `sentAt`, `organizationId`, `campusId`, `createdById`, `updatedById`, `createdAt`, `updatedAt`.
Event DTO fields: `id`, `title`, `date` (from `event_date`), `type` (from `event_type`), `roles`, `organizationId`, `campusId`, `createdById`, `updatedById`, `createdAt`, `updatedAt`.
## Access Rules
- All endpoints require an authenticated tenant user (`assertAuthenticatedTenantUser`).
- `POST /events` additionally requires manage access (`assertCanManageCommunications`): the user must hold one of `COMMUNICATION_MANAGER_ROLE_NAMES` (super admin, admin, platform owner, tenant director, campus manager) or have global access; otherwise `ForbiddenError`.
- Listing and creating parent messages requires only an authenticated tenant user.
- The frontend does not send organization, campus, creator, or updater fields. The backend fills them from the authenticated user.
## Tenant Scope
- `GET /parent-messages` filters by organization via `getOrganizationIdOrGlobal(currentUser)`:
global access users see messages across all organizations; regular users see only their own org.
Global access users also see all users' messages; regular users see only their own (`createdById`).
Audience is always `guardians`, plus `campusScope(currentUser, COMMUNICATION_TENANT_WIDE_ROLE_NAMES)`.
- `GET /events` filters by organization via `getOrganizationIdOrGlobal(currentUser)` plus the same
`campusScope`. Global access users see events across all organizations.
- `campusScope` (from `services/shared/access.ts`): tenant-wide roles (`COMMUNICATION_TENANT_WIDE_ROLE_NAMES` — super admin, admin, platform owner, tenant director) or global access see all of the organization; other users are restricted to their profile `campusId` when one is present.
- On create, `organizationId` and `campusId` are derived from the user (`getOrganizationIdOrGlobal`, `getCampusId`).
## Data Contract
- Parent message input (`ParentMessageInput`): `recipientName` (required non-empty string), `messageText` (required non-empty string), `category` (optional; mapped to one of `behavior`, `event`, `progress`, `general`, defaulting to `general`).
- Event input (`EventInput`): `title` (required), `date` (required, stored to `event_date`), `type` (required; must be one of `meeting`, `drill`, `event`, `deadline`), `roles` (optional array of role names; an empty/missing array defaults to `['teacher','support_staff','office_manager','director']`; invalid values throw `ValidationError`).
- `communication_events` model: `title` (text), `event_date` (DATEONLY), `event_type` (ENUM of the four types), `roles` (JSONB, default `[]`), `organizationId` (not null), nullable `campusId`, `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
- `createParentMessage` runs inside `withTransaction`: creates a `messages` row (`subject = category`, `body = messageText`, `channel = in_app`, `audience = guardians`, `sent_at = now`, `status = sent`) and a matching `message_recipients` row (`recipient_type = guardian`, `recipient_label = recipientName`, `delivery_status = sent`, `delivered_at = now`), then re-reads the message with its recipient to build the DTO.
- Parent-message list includes `message_recipients` (alias `message_recipients_message`, only `recipient_label`) and orders by `sent_at desc`, then `createdAt desc`.
- Event list orders by `event_date asc`, then `createdAt desc`. `createEvent` is a single create (no transaction).
- Validation failures throw `ValidationError`; access failures throw `ForbiddenError`.
## Tests
None yet (no `*.test.ts` under `backend/src` references this slice).
## Related
- Frontend: `frontend/docs/communications-integration.md`.
- Related backend slice: content catalog (`backend/docs/content-catalog.md`) backs safety protocols and parent-message templates referenced by the communications UI.