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

7.1 KiB

Campus Attendance Backend

Purpose

The campus attendance slice owns campus attendance system links (campus_attendance_config) and manually entered daily campus attendance summaries (campus_attendance_summaries), both scoped per organization. The backend is the source of truth for these records. The UI works with daily aggregate totals, not student-level attendance sessions; student-level data remains in the separate generated attendance_sessions / attendance_records models and is not handled here.

Slice Files (by layer)

  • Route: src/routes/campus_attendance.ts (thin wiring; GET /configs, PUT /configs/:campusKey, GET /summaries, PUT /summaries/:campusKey/:date). Mounted at /api/campus_attendance behind the authenticated middleware in src/index.ts.
  • Controller: src/api/controllers/campus_attendance.controller.ts (custom — listConfigs, upsertConfig, listSummaries, upsertSummary).
  • Service (BLL): src/services/campus_attendance.ts (+ src/services/campus_attendance.types.ts). Contains its own validation, scope resolution, and DTO mappers.
  • Repository (DAL): queries run through db.campus_attendance_config and db.campus_attendance_summaries inside the service (no separate db/api file).
  • Models: src/db/models/campus_attendance_config.ts, src/db/models/campus_attendance_summaries.ts.
  • Shared used: db/with-transaction.ts, services/shared/access.ts, services/shared/validate.ts (clampLimit, requiredIsoDate), shared/constants/campus-attendance.ts (role lists, limits, normalizeCampusKey, getProductRole), shared/constants/pagination.ts (resolvePagination), shared/errors/forbidden.ts, shared/errors/validation.ts.

API

All routes require JWT authentication.

  • GET /api/campus_attendance/configs -> 200 { rows, count }. rows are config DTOs. Optional query campusKey; supports limit and page via resolvePagination.
  • PUT /api/campus_attendance/configs/:campusKey -> 200 the upserted config DTO. Body is req.body.data with optional attendance_link.
  • GET /api/campus_attendance/summaries -> 200 { rows, count }. rows are summary DTOs. Optional query campusKey, startDate, endDate (ISO YYYY-MM-DD), limit.
  • PUT /api/campus_attendance/summaries/:campusKey/:date -> 200 the upserted summary DTO. :date must be an ISO date. Body is req.body.data.

Config DTO fields: id, campus_key, attendance_link, updated_by_label, organizationId, campusId, createdById, updatedById, createdAt, updatedAt.

Summary DTO fields: id, campus_key, date (from attendance_date), total_enrolled, total_present, total_absent, total_tardy, attendance_percentage (number), recorded_by_label, notes, organizationId, campusId, createdById, updatedById, createdAt, updatedAt.

Access Rules

  • All endpoints require an authenticated tenant user (assertAuthenticatedTenantUser).
  • Mutations (PUT config / summary) additionally require manage access (assertCanManageCampusAttendance): the user must hold one of CAMPUS_ATTENDANCE_MANAGER_ROLE_NAMES (super_admin, system_admin, owner, superintendent, director, office_manager). Global-access roles pass hasRoleAccess.
  • Campus-key access (assertCanAccessCampusKey): tenant-wide roles (CAMPUS_ATTENDANCE_TENANT_WIDE_ROLE_NAMES — super admin, admin, platform owner) may access any campus key. Other users may only access the campus key derived from their own profile (campus code/name, or staff profile campus code/name, normalized via normalizeCampusKey); a mismatch or missing campus key throws ForbiddenError.
  • The frontend does not send organization, campus UUID, creator, updater, or label fields. The backend derives them from the authenticated user (requireOrganizationId, getCampusId, getDisplayName, currentUser.id).

Tenant Scope

  • Every read and write filters by organizationId: requireOrganizationId(currentUser).
  • campusScope resolves the campus_key filter: a requested campusKey is access-checked then applied; tenant-wide roles with no requested key see all campus keys (no campus_key filter); other users are restricted to their own derived campus_key, and users with no derivable campus key are rejected with ForbiddenError.
  • On upsert, the existing-row lookup keys on organizationId + campus_key (config) or organizationId + campus_key + attendance_date (summary).

Data Contract

  • Config mutation input (ConfigInput): optional attendance_link (stored as trimmed text or null).
  • Summary mutation input (SummaryInput): total_enrolled, total_present, total_absent are required non-negative integers; total_tardy is optional (defaults to 0); notes optional text. Validation requires total_enrolled > 0 and that present/absent/tardy each do not exceed enrolled.
  • attendance_percentage is computed by the backend as (total_present / total_enrolled) * 100, stored as DECIMAL(5,2) and returned as a number.
  • Models: both tables carry organizationId (not null), nullable campusId, createdById (not null), nullable updatedById, paranoid soft deletes, and belongsTo associations to organizations, campuses, and users (createdBy, updatedBy). campus_attendance_config adds attendance_link and updated_by_label; campus_attendance_summaries adds attendance_date (DATEONLY), the four totals, attendance_percentage, recorded_by_label, and notes.
  • List pagination: configs use resolvePagination(limit, page); summaries use clampLimit(limit, CAMPUS_ATTENDANCE_DEFAULT_LIMIT=120, CAMPUS_ATTENDANCE_MAX_LIMIT=366) and have no offset paging.

Behavior / Notes

  • Both upserts run inside withTransaction: find existing row, then update it or create a new one (setting createdById on create).
  • Config list orders by campus_key asc; summary list orders by attendance_date desc, then campus_key asc.
  • Summary date range filtering uses requiredIsoDate on startDate/endDate and applies Op.gte / Op.lte on attendance_date.
  • Invalid campus keys, dates, or summary payloads throw ValidationError; access failures throw ForbiddenError.

Source-of-truth contract (Workstream 12)

Per the customer decision (2026-06-11), the source of truth for campus attendance is manual entry by the office_manager (and the higher campus/tenant roles), via the PUT config/summary endpoints guarded by the FILL_ATTENDANCE permission. There is no automatic derivation from student-level records.

Import from external office applications is deferred: the customer expects to import these aggregates from other office apps in future, but the specific applications are not yet known, so no import endpoint is built now. When an application is chosen, add a dedicated import endpoint (server-side validated, same tenant/campus scoping) alongside the manual path; until then manual entry is the only writer. No frontend-only attendance source exists — every UI value traces to a campus_attendance_summaries row.

Tests

None yet (no *.test.ts under backend/src references this slice).

  • Frontend: frontend/docs/campus-attendance-integration.md.