40227-vm/backend/docs/communications.md
2026-06-10 18:27:19 +02:00

6.3 KiB

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 (PRODUCT_ROLE_VALUES), 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 product-role values; an empty/missing array defaults to ['teacher','para','office','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).

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