# Documents Module — Architecture Map

> **Last updated:** 2026-06-10
> **Status:** Living document — incrementally updated as new information is discovered

---

## 1. Purpose & Responsibilities

The Documents module manages **file uploads and storage for member records**. It handles:
- Uploading documents (PDF, JPG, PNG) linked to members
- Categorization by document type (21 predefined types)
- Document preview and download
- Soft-delete (archive) with audit trail
- Cross-entity document linking (related_entity_type + related_entity_id)
- Enriching member profiles with document lists via EventBus

It does **NOT** directly manage:
- Employee documents (HR module has its own `hr_employee_documents` table)
- Player documents (SportsActivity has `sa_player_documents` table)
- Physical file storage backend (uses local filesystem under `storage/uploads/documents/`)
- Access control beyond permission checks (no per-document ownership enforcement)

---

## 2. Directory & File Structure

```
app/Modules/Documents/
├── bootstrap.php                          # Permission registration + member.profile_data listener
├── Routes.php                             # Web routes (7 routes)
├── Controllers/
│   └── DocumentController.php            # Full controller (index, upload, download, preview, archive, memberDocs)
├── Models/
│   └── Document.php                      # Model (search, getForMember, getForEntity, countForMember, types)
└── Views/
    ├── index.php                         # Document list with filters
    └── upload.php                        # Upload form for a specific member
```

---

## 3. Database Schema (Production — Source of Truth)

### 3.1 `documents` Table (5 rows as of 2026-06-10)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| member_id | bigint unsigned | NO | MUL | FK to members |
| document_type | varchar(50) | NO | MUL | One of 21 predefined types |
| original_filename | varchar(500) | NO | | User's original filename |
| stored_filename | varchar(500) | NO | | Server filename (doc_{memberId}_{timestamp}_{random}.ext) |
| file_path | varchar(1000) | NO | | Relative path from app root |
| file_size | int unsigned | NO | | File size in bytes (default: 0) |
| mime_type | varchar(100) | NO | | Detected MIME type |
| related_entity_type | varchar(100) | YES | MUL | Optional: link to another entity type |
| related_entity_id | bigint unsigned | YES | | Optional: FK to that entity |
| description | text | YES | | Free-text description |
| uploaded_by | bigint unsigned | YES | | FK to employees |
| uploaded_at | timestamp | NO | | Auto-set on creation |
| is_archived | tinyint(1) | NO | MUL | Soft delete flag (default: 0) |
| archived_at | timestamp | YES | | When archived |
| archived_by | bigint unsigned | YES | | FK to employees |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | On update cascade |

**Current data distribution (document_type, non-archived):**
- photo: 2
- birth_certificate: 2
- national_id: 1

---

## 4. Routes (7 total)

| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /documents | DocumentController@index | document.view |
| GET | /documents/upload/{memberId} | DocumentController@upload | document.upload |
| POST | /documents/upload/{memberId} | DocumentController@upload | document.upload |
| GET | /documents/{id}/download | DocumentController@download | document.view |
| GET | /documents/{id}/preview | DocumentController@preview | document.view |
| POST | /documents/{id}/archive | DocumentController@archive | document.delete |
| GET | /documents/member/{memberId} | DocumentController@memberDocuments | document.view |

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `document.uploaded` | `{document_id, member_id, type, filename}` | DocumentController::upload |
| `document.archived` | `{document_id, member_id}` | DocumentController::archive |

---

## 6. Events Consumed (by this module)

| Event | Action |
|-------|--------|
| `member.profile_data` | Injects `$data['documents']` array into member profile view (via Document::getForMember) |

**No other modules listen to document events** (document.uploaded, document.archived have no external listeners).

---

## 7. Cross-Module Dependencies

### 7.1 Documents module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Members | members table (for validation that member exists) |
| Core | App, EventBus, Logger, Pagination |

The Documents module is very lightweight with minimal external dependencies.

### 7.2 Other modules that IMPORT FROM Documents:

| Module | What it imports/references |
|--------|--------------------------|
| Members | Sidebar menu links to /documents; member profile view shows document tab |
| Members | bootstrap.php registers 'document.view' permission in menu children |

**Note:** HR and SportsActivity have their OWN separate document systems (not using this module's Document model):
- HR: `hr_employee_documents` table, `HrEmployeeDocument` model, `/hr/documents` routes
- SportsActivity: `sa_player_documents` table, `PlayerDocument` model, `/sa/players/{pid}/documents` routes

---

## 8. Permissions

| Key | Description |
|-----|-------------|
| document.upload | Upload documents to a member's file |
| document.view | View and download documents |
| document.delete | Archive (soft-delete) documents |

---

## 9. Core Business Flows

### 9.1 Upload Document

```
1. Validate member exists and is not archived
2. Validate POST data:
   - document_type is required
   - File is present and upload succeeded
   - File size <= 10MB (configurable via filestorage.max_upload_size)
   - MIME type is PDF, JPEG, or PNG (validated via finfo)
3. Generate stored filename: doc_{memberId}_{Ymd_His}_{random_hex}.{ext}
4. Create upload directory if needed: storage/uploads/documents/
5. Move uploaded file to storage location
6. Insert document record with metadata
7. Dispatch document.uploaded event
8. Redirect back with success message
```

### 9.2 Download / Preview

```
1. Find document by ID
2. Check not archived
3. Verify physical file exists on disk
4. Download: Send as attachment with original filename
5. Preview: Send inline with detected MIME type (renders in browser for PDF/images)
```

### 9.3 Archive (Soft Delete)

```
1. Find document by ID
2. Set is_archived=1, archived_at, archived_by
3. Dispatch document.archived event
4. Redirect to custom redirect_url or /documents
```

**Note:** Physical file is NOT deleted from disk on archive.

---

## 10. Document Types (21 Predefined)

| Code | Arabic Label |
|------|-------------|
| national_id | صورة الرقم القومي |
| birth_certificate | شهادة ميلاد |
| marriage_certificate | عقد زواج |
| divorce_certificate | وثيقة طلاق |
| death_certificate | شهادة وفاة |
| form_scan | نسخة الاستمارة |
| qualification | شهادة المؤهل |
| passport | صورة جواز السفر |
| work_letter | خطاب العمل |
| photo | صورة شخصية |
| disability_doc | مستند إعاقة |
| championship_doc | مستند بطولة |
| medical_certificate | شهادة طبية |
| fitness_cert | شهادة لياقة بدنية |
| guardian_consent | موافقة ولي الأمر |
| federation_card | كارنيه الاتحاد |
| player_photo | صورة اللاعب |
| rental_letter | خطاب رسمي للتأجير |
| technical_report | تقرير فني |
| contract_scan | نسخة عقد |
| other | مستند آخر |

---

## 11. High-Risk Areas

### 11.1 File Storage Security
- Files stored under `storage/uploads/documents/` in the application directory
- No access control at filesystem level — anyone with the stored_filename could potentially access
- The preview endpoint bypasses Response class (uses raw `header()` + `readfile()` + `exit`) — no middleware runs after authorization
- No virus scanning or content validation beyond MIME type check

### 11.2 No Orphan File Cleanup
- When a document is archived, the physical file remains on disk indefinitely
- No cron job or cleanup mechanism to remove archived files
- Over time, disk usage will grow with no reclamation

### 11.3 Related Entity Link (unused but available)
- `related_entity_type` and `related_entity_id` columns exist and can link documents to any entity
- Currently not actively used in the upload UI (fields are in the POST handler but not prominently exposed)
- No FK constraint — can point to non-existent entities
- `Document::getForEntity()` method exists but no code currently calls it from other modules

### 11.4 Preview Endpoint Exits PHP
- `DocumentController::preview()` calls `exit` after `readfile()` — this bypasses the framework's response pipeline
- No after-middleware, no session save, no error handling wraps this
- If the file is large, memory issues possible (readfile streams, but no explicit memory limit handling)

### 11.5 Upload Size Validation
- Uses `config('filestorage.max_upload_size', 10485760)` — but PHP's `upload_max_filesize` and `post_max_size` may reject files before the code runs
- No client-side size validation in the form

---

## 12. Known Patterns & Gotchas

1. **Member-centric only**: All documents must have a member_id — no standalone document uploads
2. **Soft delete uses is_archived**: Standard pattern, queries filter by `is_archived = 0`
3. **No versioning**: Re-uploading a document type creates a new record, doesn't replace the old one
4. **uploaded_at vs created_at**: The table has both; `uploaded_at` is used for display ordering, `created_at` is the framework timestamp
5. **Member profile integration**: Documents module injects document list into member profile via `member.profile_data` event — this runs every time a member profile loads
6. **Three separate document systems exist**: Members (this module), HR employees, SA players — they do NOT share tables or models
