# Archive Module — Architecture Map

> **Last updated:** 2026-06-22 (added member-centric Archive screen in Members module)
> **Status:** Living document — incrementally updated as new information is discovered

---

## 1. Purpose & Responsibilities

The Archive module provides a **point-in-time snapshot system** for entities in the ERP, plus a **membership number ownership chain** tracker. It serves as the system's historical record and audit trail for major lifecycle events:

- Takes full JSON snapshots of entities (members, employees, roles) before destructive operations
- Auto-loads related data for known entity types (spouses, children, payments, etc.)
- Provides snapshot comparison (diff) between two points in time
- Tracks the complete ownership history of membership numbers across transfers, deaths, divorces, and waivers
- Supports manual snapshot creation by authorized users
- Provides search, filter, and browse capabilities across all archived snapshots

It does **NOT**:
- Perform soft-delete archival (that is handled by each module's `is_archived` flag)
- Replace the audit_trail table (which logs individual field changes)
- Restore entities from snapshots (read-only historical record)

### Member-Centric Archive View (Added 2026-06-22)

In addition to the technical snapshot browser (`/archive`), a **user-friendly archive screen** exists inside the Members module at `/members/archive`. This provides:
- Browsable list of all archived members (is_archived=1) with advanced search filters
- Detailed show page per archived member: personal data, dependents, financial history, number chain, audit trail
- Direct linking between old and new members (who received the membership number)
- Permission: `member.archive`

Files:
- `app/Modules/Members/Controllers/MemberArchiveController.php` — index + show
- `app/Modules/Members/Views/archive_index.php` — searchable listing
- `app/Modules/Members/Views/archive_show.php` — full detail with sidebar

---

## 2. Directory & File Structure

```
app/Modules/Archive/
├── bootstrap.php                          # Menu + permission registration (child of branches_settings)
├── Routes.php                             # 6 routes (index, show, compare, entity, number-chain, snapshot)
├── Controllers/
│   └── ArchiveController.php             # Browsing, comparison, manual snapshot creation
├── Models/
│   ├── ArchiveSnapshot.php               # Snapshot storage + search (archive_snapshots table)
│   └── MembershipNumberChain.php         # Number ownership tracking (membership_number_chain table)
├── Services/
│   └── ArchiveService.php                # Core operations: takeSnapshot, compareSnapshots, recordNumberTransfer
└── Views/
    ├── index.php                          # Paginated snapshot list with filters
    ├── show.php                           # Single snapshot detail + number chain view
    └── compare.php                        # Side-by-side snapshot comparison
```

---

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

### 3.1 `archive_snapshots` Table (6 rows)

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| entity_type | varchar(100) | NO | MUL | | Table name (e.g., 'members', 'employees') |
| entity_id | bigint unsigned | NO | | | PK of the snapshotted entity |
| membership_number | varchar(20) | YES | MUL | NULL | Cross-reference for member-related snapshots |
| snapshot_reason | varchar(50) | NO | MUL | | Why: death, divorce, waiver, transfer, manual |
| full_data_json | json | NO | | | Complete row from source table |
| related_data_json | json | YES | | NULL | Related records (spouses, children, payments, etc.) |
| snapshot_taken_by | bigint unsigned | YES | | NULL | FK to employees |
| snapshot_taken_at | timestamp | NO | MUL | CURRENT_TIMESTAMP | When snapshot was captured |
| notes | text | YES | | NULL | Human-readable context |

**Composite index**: `(entity_type, entity_id)` for fast entity-specific lookups.

### 3.2 `membership_number_chain` Table (6 rows)

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| membership_number | varchar(20) | NO | MUL | | The membership number being tracked |
| holder_type | varchar(50) | NO | | | How obtained: original, waiver, death_transfer, divorce |
| holder_entity_type | varchar(100) | NO | MUL | | Entity type of holder (e.g., 'members') |
| holder_entity_id | bigint unsigned | NO | | | PK of current holder |
| previous_holder_id | bigint unsigned | YES | MUL | NULL | FK to previous chain entry |
| held_from | datetime | NO | | | Start of ownership |
| held_until | datetime | YES | | NULL | End of ownership (NULL = current holder) |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |

**Key query pattern**: `WHERE membership_number = ? AND held_until IS NULL` — finds current holder.

---

## 4. Routes

| Method | Path | Handler | Middleware | Permission |
|--------|------|---------|------------|------------|
| GET | /archive | ArchiveController@index | auth | report.view_audit |
| GET | /archive/{id} | ArchiveController@show | auth | report.view_audit |
| GET | /archive/compare/{id1}/{id2} | ArchiveController@compare | auth | report.view_audit |
| GET | /archive/entity/{type}/{id} | ArchiveController@entitySnapshots | auth | report.view_audit |
| GET | /archive/number-chain/{number} | ArchiveController@numberChain | auth | report.view_audit |
| POST | /archive/snapshot | ArchiveController@takeManual | auth, csrf | settings.edit |

---

## 5. Events Dispatched

None. The Archive module does not dispatch any events.

---

## 6. Events Consumed

None. The Archive module does not listen to any EventBus events. It is invoked directly by other modules' services.

---

## 7. Cross-Module Dependencies

### 7.1 Archive module IMPORTS FROM (via SQL in autoLoadRelatedData):

| Module / Table | What it loads |
|----------------|---------------|
| Members | `members` — primary entity for snapshots |
| Spouses | `spouses` — related data for member snapshots |
| Children | `children` — related data for member snapshots |
| Temporary | `temporary_members` — related data for member snapshots |
| Payments | `payments` — related data for member snapshots |
| Installments | `installment_plans` — related data for member snapshots |
| Subscriptions | `subscriptions` — related data for member snapshots |
| Fines | `fines` — related data for member snapshots |
| Documents | `documents` — related data for member snapshots |
| Seasonal | `seasonal_memberships` — related data for member snapshots |
| SportsActivity | `sports_members` — related data for member snapshots |
| Honorary | `honorary_members` — related data for member snapshots |
| Foreign | `foreign_member_details` — related data for member snapshots |
| HR | `employees`, `employee_roles`, `roles` — employee snapshots |
| Settings | `role_permissions` — role snapshots |

### 7.2 Other modules that IMPORT FROM Archive:

| Module | What it imports | Usage |
|--------|----------------|-------|
| Waiver | `ArchiveService::takeSnapshot()`, `ArchiveService::recordNumberTransfer()` | Snapshot before waiver, record number transfer |
| Death | `ArchiveService::takeSnapshot()`, `ArchiveService::recordNumberTransfer()` | Snapshot before death processing, record number inheritance |
| Divorce | `ArchiveService::takeSnapshot()`, `ArchiveService::recordNumberTransfer()` | Snapshot before divorce split, record number transfer |
| Transfers | `ArchiveService::takeSnapshot()`, `ArchiveService::recordNumberTransfer()` | Snapshot before transfer, record number reassignment |

---

## 8. Permissions

### Registered Permissions

| Key | Description |
|-----|-------------|
| archive.view | View archive snapshots |
| archive.take_snapshot | Take a manual archive snapshot |
| archive.compare | Compare two snapshots |
| archive.number_chain | View membership number chain |

### Permissions Used in Routes

| Route Permission | Source |
|-----------------|--------|
| report.view_audit | From Reports module — used for all read routes |
| settings.edit | From Settings module — used for manual snapshot creation |

**Note**: The module registers its own permissions but the routes actually use permissions from other modules (`report.view_audit`, `settings.edit`). The registered permissions (`archive.view`, etc.) appear unused in routes.

**Menu placement**: Child of `branches_settings` menu, order 4.

---

## 9. Background Processes (Cron)

None. Snapshots are taken on-demand by other modules during lifecycle events.

---

## 10. High-Risk Areas

### 10.1 Raw Table Name in SQL (SQL Injection Vector)

`ArchiveService::takeSnapshot()` interpolates `$entityType` directly into SQL:
```php
$entityData = $db->selectOne("SELECT * FROM `{$entityType}` WHERE id = ?", [$entityId]);
```

While callers currently pass trusted table names ('members', 'employees', 'roles'), any untrusted input to `entityType` could lead to SQL injection. The manual snapshot POST endpoint accepts `entity_type` from user input.

### 10.2 Unbounded JSON Storage

`full_data_json` and `related_data_json` store entire entity trees as JSON. For a member with many payments, installments, subscriptions, and fines, this could be several MB per snapshot. No size limits or pruning exist.

### 10.3 tableExists() Calls in Hot Path

`autoLoadRelatedData()` calls `$db->tableExists()` for 12+ tables on every member snapshot. These are information_schema queries that may be slow under load.

### 10.4 No Integrity Enforcement

- `entity_type` + `entity_id` reference is not validated (entity may be deleted)
- `previous_holder_id` in chain is not a true FK constraint
- `snapshot_taken_by` has no FK constraint to employees table

### 10.5 Permission Mismatch

Routes use `report.view_audit` and `settings.edit` but the module registers its own permission set (`archive.view`, `archive.take_snapshot`, etc.). This means:
- Role assignments for `archive.view` have no effect on access
- Access is actually controlled by `report.view_audit` permission

---

## 11. Known Patterns & Gotchas

1. **Entity type is a raw table name**: `entity_type` stores the literal MySQL table name (e.g., 'members'), not a logical entity name
2. **Compare requires same entity**: The comparison view doesn't enforce that both snapshots are for the same entity — comparing unrelated snapshots will show misleading diffs
3. **Number chain is append-only**: `endCurrentHolder()` sets `held_until` on the existing record, then a new record is inserted — no deletion possible
4. **Snapshot JSON includes sensitive data**: Member national_id, phone numbers, addresses are all captured in `full_data_json` — no PII redaction
5. **No cascade on entity deletion**: If a member is hard-deleted, their snapshots remain with dangling `entity_id` references
6. **String comparison in diff**: `diffArrays()` casts all values to strings for comparison — `0` and `'0'` and `null` and `''` may produce unexpected diff results
7. **Show view doubles as number-chain view**: The `show.php` template handles both snapshot detail and number chain display via an `$isChainView` flag

---

## 12. Auto-Load Related Data Behavior

When a snapshot is taken for a `members` entity, the service automatically captures:

| Related Table | Condition |
|--------------|-----------|
| spouses | member_id = ?, is_archived = 0 |
| children | member_id = ?, is_archived = 0 |
| temporary_members | member_id = ?, is_archived = 0 |
| payments | member_id = ?, is_voided = 0 |
| installment_plans | member_id = ? |
| subscriptions | member_id = ? (ordered by financial_year DESC) |
| fines | member_id = ? |
| documents | member_id = ?, is_archived = 0 (limited columns) |
| seasonal_memberships | member_id = ?, is_archived = 0 |
| sports_members | member_id = ?, is_archived = 0 |
| honorary_members | member_id = ?, is_archived = 0 |
| foreign_member_details | member_id = ?, is_archived = 0 |

For `employees` entities: loads roles via employee_roles + roles join.
For `roles` entities: loads role_permissions.

---

## 13. Configuration Dependencies

None. The Archive module has no system_config, rule engine, or service catalog dependencies.
