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_attendancebehind theauthenticatedmiddleware insrc/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_configanddb.campus_attendance_summariesinside the service (no separatedb/apifile). - 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 }.rowsare config DTOs. Optional querycampusKey; supportslimitandpageviaresolvePagination.PUT /api/campus_attendance/configs/:campusKey->200the upserted config DTO. Body isreq.body.datawith optionalattendance_link.GET /api/campus_attendance/summaries->200{ rows, count }.rowsare summary DTOs. Optional querycampusKey,startDate,endDate(ISOYYYY-MM-DD),limit.PUT /api/campus_attendance/summaries/:campusKey/:date->200the upserted summary DTO.:datemust be an ISO date. Body isreq.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 (
PUTconfig / summary) additionally require manage access (assertCanManageCampusAttendance): the user must hold one ofCAMPUS_ATTENDANCE_MANAGER_ROLE_NAMES(super_admin,system_admin,owner,superintendent,director,office_manager). Global-access roles passhasRoleAccess. - 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 vianormalizeCampusKey); a mismatch or missing campus key throwsForbiddenError. - 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). campusScoperesolves thecampus_keyfilter: a requestedcampusKeyis access-checked then applied; tenant-wide roles with no requested key see all campus keys (nocampus_keyfilter); other users are restricted to their own derivedcampus_key, and users with no derivable campus key are rejected withForbiddenError.- On upsert, the existing-row lookup keys on
organizationId+campus_key(config) ororganizationId+campus_key+attendance_date(summary).
Data Contract
- Config mutation input (
ConfigInput): optionalattendance_link(stored as trimmed text ornull). - Summary mutation input (
SummaryInput):total_enrolled,total_present,total_absentare required non-negative integers;total_tardyis optional (defaults to0);notesoptional text. Validation requirestotal_enrolled > 0and that present/absent/tardy each do not exceed enrolled. attendance_percentageis computed by the backend as(total_present / total_enrolled) * 100, stored asDECIMAL(5,2)and returned as a number.- Models: both tables carry
organizationId(not null), nullablecampusId,createdById(not null), nullableupdatedById,paranoidsoft deletes, andbelongsToassociations toorganizations,campuses, andusers(createdBy, updatedBy).campus_attendance_configaddsattendance_linkandupdated_by_label;campus_attendance_summariesaddsattendance_date(DATEONLY), the four totals,attendance_percentage,recorded_by_label, andnotes. - List pagination: configs use
resolvePagination(limit, page); summaries useclampLimit(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, thenupdateit orcreatea new one (settingcreatedByIdon create). - Config list orders by
campus_key asc; summary list orders byattendance_date desc, thencampus_key asc. - Summary date range filtering uses
requiredIsoDateonstartDate/endDateand appliesOp.gte/Op.lteonattendance_date. - Invalid campus keys, dates, or summary payloads throw
ValidationError; access failures throwForbiddenError.
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).
Related
- Frontend:
frontend/docs/campus-attendance-integration.md.