# M08 — Documents & Evidence

Phase 4. Versioned, confidentiality-tiered document management built on
`spatie/laravel-medialibrary`, plus an evidence/exhibit register for litigation
matters.

## Dependencies
M03 (matters), M04 (hearings, for evidence tendering).

## Design: media library, not a bespoke files table

Use `spatie/laravel-medialibrary` for storage/conversions/collections rather
than a hand-rolled documents table — saves significant plumbing and is
well-audited. Domain layer (versioning, legal typing, confidentiality) sits on
top via a `documents` table that wraps a media collection.

## Database

### `documents`
```php
$table->id(); $table->ulid('ulid')->unique();
$table->foreignId('firm_id')->constrained()->restrictOnDelete();
$table->foreignId('matter_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('client_id')->nullable()->constrained()->nullOnDelete();
$table->string('title');
$table->string('legal_type')->nullable();
   // pleading / motion / affidavit / written_address / judgment / ruling /
   // contract / correspondence / evidence / court_process / opinion / other
$table->string('confidentiality')->default('standard'); // standard/confidential/privileged
$table->unsignedInteger('current_version')->default(1);
$table->foreignId('uploaded_by')->constrained('users');
$table->timestamps(); $table->softDeletes();
$table->fullText(['title']);
```
Uses `HasMedia` trait; media collection `versions` holds every uploaded file
(medialibrary's built-in versioning-friendly storage), each media item carries
custom properties `version_number`, `change_note`, `sha256`.

### `document_privileged_access` (ethical wall allow-list)
```php
$table->foreignId('document_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('granted_by')->constrained('users');
$table->timestamps();
$table->unique(['document_id', 'user_id']);
```
Only populated/managed when `confidentiality = privileged`; Firm Admin only.

### `evidence_items`
```php
$table->id();
$table->foreignId('matter_id')->constrained()->cascadeOnDelete();
$table->foreignId('document_id')->nullable()->constrained()->nullOnDelete();
$table->string('exhibit_number');
$table->string('title'); $table->string('side')->nullable();
$table->text('description')->nullable();
$table->text('custody_notes')->nullable();
$table->string('status')->default('proposed'); // proposed/tendered/admitted/rejected
$table->foreignId('hearing_id')->nullable()->constrained()->nullOnDelete();
$table->timestamps();
$table->unique(['matter_id', 'exhibit_number']);
```

## Authorization: `DocumentPolicy`

```
view:      confidentiality=standard → matter team OR documents.viewAll
           confidentiality=confidential → matter team AND documents.viewConfidential
           confidentiality=privileged → explicit document_privileged_access row only
upload/uploadVersion/manageEvidence: matter team OR relevant permission
```
Every download flows through a controller action that calls `authorize()` then
streams via `Storage`/medialibrary response — never a direct public media URL
for anything above `standard` confidentiality (medialibrary's private disk
config handles this).

## Versioning behaviour

`App\Actions\Documents\UploadNewVersion::handle(Document $doc, UploadedFile $file, ?string $note, User $actor)`:
increments `current_version`, adds a new media item (never replaces/deletes a
prior one), computes and stores `sha256` for integrity, writes activity log
+ `matter_events` ("document_added"/"document_versioned"). Version history view
lists every version with uploader, date, note, and a per-version authorized
download link.

## Search

`GET /documents/search` — filters: matter, client, legal_type, uploader, date
range, confidentiality; full-text on `title` (MySQL FULLTEXT) combined with
filter `WHERE`s. Result set always passes through the same Policy check before
rendering (no leaking titles of privileged documents in search results either —
filter at the query level via the same visibility rule, not just at render time).

## Template-based document generation

`document_generation_templates` (Blade or DOCX templates with merge fields:
`{{client.name}}`, `{{matter.suit_number}}`, `{{matter.title}}`, firm letterhead)
→ `GenerateDocumentFromTemplate` Action renders to PDF (DomPDF, queued) and
saves as a new `documents` row (`legal_type` matches template's default,
version 1). Start with: engagement letter, invoice cover sheet, hearing notice,
receipt.

## Routes

```
RESTful /documents                       (index/create/store/show)
POST /documents/{document}/versions      documents.versions.store
GET  /documents/{document}/versions/{v}/download  documents.versions.download
GET  /documents/search                   documents.search
RESTful /matters/{matter}/evidence        (evidence_items)
POST /evidence/{item}/status             evidence.status.update
GET  /matters/{matter}/evidence/print     evidence.print (queued PDF)
POST /documents/{document}/privileged-access  documents.privileged.grant/revoke
RESTful /document-generation-templates   (admin)
POST /matters/{matter}/generate-document (documents.generate)
```

## Acceptance criteria

- [ ] Document upload + versioning: every version retained, never overwritten;
      sha256 stored per version
- [ ] Confidentiality tiers enforced by Policy, not just UI — tested for all
      three tiers including privileged allow-list
- [ ] Every download authorized + activity-logged
- [ ] Search respects confidentiality (privileged docs never appear to
      non-allow-listed users, even in result counts)
- [ ] Evidence register: unique exhibit numbers per matter; tendering links a
      hearing; printable exhibit list PDF renders
- [ ] Document generation from template produces a correctly merged PDF, saved
      as version 1 of a new document
- [ ] Cross-firm isolation tests green

## Claude Code kickoff prompt

> Read CLAUDE.md, docs/04-SECURITY.md §5, and this file. Install and configure
> `spatie/laravel-medialibrary` on a private disk first. Implement: documents +
> HasMedia setup → DocumentPolicy (write the three-tier + privileged-allow-list
> test FIRST) → UploadNewVersion Action → authorized download controller →
> search (with confidentiality-safe query, not post-filter) → evidence register
> → document generation templates. No document content should ever be
> reachable via a guessable/public URL — verify this explicitly in tests.
