Files + comments — evidence + collaboration
Two polymorphic stores attached to every job + every task: job_attachments (file uploads) and comments (threaded discussion). Both support job-level + task-level scope. RBAC-gated, internal-vs-client visibility, full audit trail.
Files — what attaches where
| Scope | Use for | Path on disk |
|---|---|---|
| Job-level | TB import, signed engagement letter, management letter, FS pack | storage/jobs/{id}/{folder}/{uuid}.{ext} |
| Task-level | Procedure-specific evidence (bank confirmation, stock count cert, signed reconciliation) | storage/jobs/{id}/tasks/{taskid}/{uuid}.{ext} |
| Checklist-item-level | One-file-per-item proof (per item) | storage/jobs/{id}/checklist/{itemid}/{uuid}.{ext} |
| Workpaper-level (M19) | Audit evidence linked to a specific workpaper | storage/jobs/{id}/workpapers/{wpcode}/{uuid}.{ext} |
Upload pipeline
Drag-drop or click
From the Files tab → drag-drop area or "Choose file". Multiple files supported (sequential upload).
Profile lookup
Server reads the
job_attachmentfile_upload_profile row → max size (default 25 MB), allowed extensions (pdf/jpg/png/doc/docx/xls/xlsx/csv/zip), allowed MIME prefixes.Server-side validation
Three checks must all pass: extension whitelist, MIME magic-byte sniff, size cap. Fails return a 400 with the specific reason. Never relies on client-supplied content-type.
UUID rename + persist
Original filename preserved in DB column; physical file renamed to UUID to prevent path-traversal and collisions. SHA-256 computed + stored.
Audit-log entry
Action
m07.attachment.uploadwith user, file UUID, target type/id.
Download (auth + integrity-checked)
Every download goes through:
- Auth check — must be logged in
- RBAC check — user must have
m07.view+ scope match (department, assigned, all) - Path-traversal guard — re-validates the resolved path is inside the expected directory
- SHA-256 integrity check — re-hash on disk vs stored hash; mismatch returns 500 + alerts ops
- Audit-log entry — every download is recorded in
vault_download_log(user, IP, file, timestamp). M16 admin notification fires on confidential downloads.
Comments — internal vs client-visible
Each comment row has:
Either job or task. Same comments table serves any scope.
Plain text + line breaks. @mention someone to notify. Output through htmlspecialchars().
Internal-only — never shown to client. Useful for review notes, internal discussions, fee discussions.
Author + timestamp. Only the author can edit/delete (within 15 min for delete).
Comment moderation
- Edit: author-only, edits are stamped (badge "Edited 2 min ago")
- Delete: author within 15 min; super_admin / partner anytime; audit-logged
- Hide: if the comment violates firm policy, super_admin can hide it (soft-delete; preserved in DB, never shown)
- @mentions: notify the mentioned user via M17 in-app notification (and email, if user has it on)
The unified File Vault — discoverability
All these scattered job/task/workpaper attachments are surfaced in one place via M16 File Vault Explorer at /documents/explorer. Tree view: Clients → Engagements → Year → Phase folder → files. The Vault is read-only across these sources — edits go through the source module that owns the file (M07 / M19 / M04 / M16 vault).
Open a job → Files tab → upload a 5 MB PDF. Note the upload speed + audit-log entry under Activity tab. Now open File Vault → Explorer → drill into Clients → that client → that job → the same PDF appears (with a "Job file" badge). One source, two access points.
Be careful with is_internal. If you write "fee should have been higher" in a non-internal comment and your client gets a portal login later, they'll see it. Default to is_internal=1 for anything sensitive.
For audit work, attach evidence at checklist-item level, not job-level. Reason: when a regulator inspects, they ask "where's the evidence for procedure X?" — having it nested under the procedure is a 1-click answer. Job-level is the right scope only for whole-engagement files (signed engagement letter, FS pack, audit report).