40227-vm/backend/docs/campus-attendance.md

63 lines
7.1 KiB
Markdown

# Campus Attendance Backend
## Purpose
The campus attendance slice owns campus attendance system links (`campus_attendance_config`) and legacy manually entered daily campus attendance summaries (`campus_attendance_summaries`), both scoped per organization. The active frontend attendance workflow tracks staff attendance through `staff_attendance_records`; student/classroom aggregate entry has been removed from the UI. 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 `FILL_ATTENDANCE` (`assertCanManageCampusAttendance`). Global-access users still pass through the shared global-access permission bypass.
- Campus-key access (`assertCanAccessCampusKey`): organization/global active scope may access any campus key in the active organization; school scope may access campus keys under the active school; campus/class scope may access only the current campus key. 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`).
- The legacy summary mutation still writes a campus-level summary. Current frontend attendance entry no longer calls this endpoint.
## Tenant Scope
- Every read and write filters by `organizationId: requireOrganizationId(currentUser)`.
- `campusKeyScope` resolves the `campus_key` filter: a requested `campusKey` is access-checked then applied; organization/global active scope with no requested key sees all campus keys in the active organization; school scope with no requested key sees all campus keys under the active school; campus/class scope is restricted to the current 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 later staff-only attendance decision, the active attendance source of truth is `staff_attendance_records`. The campus summary endpoints remain for legacy data/API compatibility and are not used by the current frontend attendance entry form.
**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.
## Tests
- `src/api/controllers/campus_attendance.controller.test.ts` covers controller delegation.
- `src/services/campus_attendance.test.ts` covers active campus drill-down access by resolving campus UUID to the stored campus key.
## Related
- Frontend: `frontend/docs/campus-attendance-integration.md`.