# Documents Backend

## Purpose

`documents` stores file/document metadata records (with related `file` uploads) attached to
school entities such as students, staff, classes, invoices, organizations, and campuses. The
slice is hand-written (not the generic CRUD factory): the service returns trimmed DTOs via
`toDocumentDto`, supports CSV export and CSV bulk import, and resolves related `organization`,
`campus`, and `file` on single-record reads.

## Slice Files (by layer)

- Route: `src/routes/documents.ts` (wires CRUD plus `bulk-import`, `count`, `autocomplete`,
  `deleteByIds`; applies `checkCrudPermissions('documents')` to every route).
- Controller: `src/api/controllers/documents.controller.ts` (custom — maps DTOs, handles CSV
  export and file upload).
- Service (BLL): `src/services/documents.ts` (exports `toDocumentDto`; wraps writes in
  transactions; parses CSV buffers for bulk import).
- Repository (DAL): `src/db/api/documents.ts`.
- Model: `src/db/models/documents.ts`.
- Shared used: `db/api/shared/repository.ts` (`removeRecord`, `deleteRecordsByIds`,
  `autocompleteByField`), `db/api/file.ts` (`FileDBApi.replaceRelationFiles`), `db/utils.ts`
  (`Utils.uuid`, `Utils.ilike`), `shared/constants/pagination.ts` (`resolvePagination`),
  `shared/constants/database.ts` (`BULK_IMPORT_TIMESTAMP_STEP_MS`), `shared/csv.ts` (`toCsv`),
  `middlewares/upload.ts` (`processFile`), `middlewares/check-permissions.ts`,
  `shared/errors/validation.ts` (`ValidationError`).

## API

All routes are mounted under `/api/documents` and require JWT authentication (mounted with
`authenticated` in `src/index.ts`). Every route additionally passes
`checkCrudPermissions('documents')`, which checks the permission `${METHOD}_DOCUMENTS`
(see `permissions.md`).

- `POST /api/documents` -> `201`, the created document DTO. Request body: `{ data: <DocumentInput> }`.
- `POST /api/documents/bulk-import` -> `200` `true`. Multipart file upload (`processFile`); a CSV
  buffer is parsed into rows. Missing file raises `ValidationError('importer.errors.invalidFileEmpty')`.
- `PUT /api/documents/:id` -> `200`, the updated document DTO. The controller calls
  `Service.update(req.body.data, req.body.id, ...)` (it reads `req.body.id`, not `req.params.id`).
- `DELETE /api/documents/:id` -> `200` `true`.
- `POST /api/documents/deleteByIds` -> `200` `true`. Request body: `{ data: string[] }`.
- `GET /api/documents` -> `200` `{ rows, count }` where `rows` are DTOs. When `?filetype=csv`,
  responds with a CSV attachment of fields `id, entity_reference, name, notes, uploaded_at`.
- `GET /api/documents/count` -> `200` `{ rows: [], count }` (count-only).
- `GET /api/documents/autocomplete` -> `200` array of `{ id, label }` matched on `name`.
- `GET /api/documents/:id` -> `200`, a single record (plain) with eager-resolved `organization`,
  `campus`, and `file`. This response is NOT passed through `toDocumentDto`.

## Access Rules

Authorization is by CRUD permission only: `checkCrudPermissions('documents')` requires the
effective role (or a custom per-user permission) to hold `${METHOD}_DOCUMENTS`. There is no
additional role-name gate or owner check inside the service. The self-access bypass in
`check-permissions.ts` (matching `req.params.id`/`req.body.id` to the current user id) does not
meaningfully apply to documents since those ids are document ids, not user ids.

## Tenant Scope

- `findAll` scopes to `currentUser.organizationId` only when the user has a loaded
  `organizations` association and an `organizationId`. Callers with `globalAccess` (from
  `currentUser.app_role.globalAccess`) have the `organizationId` constraint removed, so they read
  across organizations.
- On `create`, the document's organization is forced to `currentUser.organizationId`
  (`setOrganization`), regardless of input.
- On `update`, organization is only changed when `data.organization` is provided: global-access
  users may set the provided organization; non-global users are pinned back to
  `currentUser.organizationId`.
- `campus` is set from input on create and update.

## Data Contract

Model columns (`src/db/models/documents.ts`): `id` (UUID PK), `entity_type` (ENUM: `student`,
`staff`, `class`, `invoice`, `organization`, `campus`, `other`), `entity_reference` (text),
`name` (text), `category` (ENUM: `policy`, `report`, `id`, `medical`, `consent`, `invoice`,
`receipt`, `other`), `uploaded_at` (date), `notes` (text), `importHash` (unique), `createdAt`,
`updatedAt`, `deletedAt` (paranoid soft-delete), `campusId`, `organizationId`, `createdById`,
`updatedById`. Associations: `belongsTo organizations` (as `organization`), `belongsTo campuses`
(as `campus`), `hasMany file` (as `file`, scoped relation upload), `belongsTo users` (as
`createdBy`/`updatedBy`).

`toDocumentDto` exposes only: `id`, `entity_type`, `entity_reference`, `name`, `category`,
`uploaded_at`, `notes`, `organizationId`, `campusId`, `createdById`, `updatedById`, `createdAt`,
`updatedAt`. It deliberately omits `importHash`, `deletedAt`, and eager relations.

List filters (`findAll`): `id`, `entity_reference`, `name`, `notes` (all ILIKE),
`uploaded_atRange`, `active`, `entity_type`, `category`, `campus` (filter-only inner join on id
or name, `|`-separated), `organization` (`|`-separated ids), `createdAtRange`, plus `field`/`sort`
ordering (defaults to `createdAt desc`) and `limit`/`page` pagination.

## Behavior / Notes

- All mutations (`create`, `bulkImport`, `update`, `deleteByIds`, `remove`) run inside a manual
  Sequelize transaction (`db.sequelize.transaction()`), committing on success and rolling back on
  error.
- `update` raises `ValidationError('documentsNotFound')` when the record does not exist.
- Bulk import parses the uploaded CSV (`csv-parser`) into rows, then `bulkCreate`s with
  `ignoreDuplicates: true` and per-row `createdAt` staggered by `BULK_IMPORT_TIMESTAMP_STEP_MS`
  to preserve ordering. Related files are attached per row via `replaceRelationFiles`.
- The list query selects only scalar columns (no eager org/file load); the `campus` filter join
  selects no attributes (filter-only, inner join).

## Tests

None yet (no `documents` unit/e2e test under `src/`).

## Related

- Frontend: `frontend/docs/policies-integration.md` (the handbook/policies workflow reads and
  mutates policy documents via `GET /api/documents?category=policy`, `POST`, `PUT`, `DELETE`).
- Backend slices: `permissions.md` (the `${METHOD}_DOCUMENTS` permission gate), and the `file`
  upload relation used by `replaceRelationFiles`.
