# Honorary Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Honorary module manages **honorary (complimentary) memberships** granted by board decision. It:
- Records honorary membership grants with reason, decision reference, and term dates
- Enforces configurable duration (default 1 year)
- Supports renewable honorary memberships with renewal tracking
- Validates the member is already typed as 'honorary' (set at creation)
- Activates honorary members directly (no payment required — board decision)
- Provides expiry monitoring (via `isExpiring()` helper)

It does **NOT** directly manage:
- The core member record (Members module owns that)
- Fee collection (honorary memberships are free)
- Automated expiry enforcement (that lives in Members/AutoFreezeService)

---

## 2. Directory & File Structure

```
app/Modules/Honorary/
├── bootstrap.php                          # No-op (menu registered by Members/bootstrap.php)
├── Routes.php                             # Web routes (3 routes)
├── Controllers/
│   └── HonoraryController.php            # Main controller (index, create, store)
├── Models/
│   └── HonoraryMember.php                # Model (soft delete, events)
└── Views/
    ├── index.php                         # All active honorary members list
    └── create.php                        # Grant honorary membership form
```

---

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

### 3.1 `honorary_members` Table (0 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 |
| granted_by_decision | varchar(200) | YES | | Board decision reference number |
| reason | text | NO | | Reason for granting honorary status |
| start_date | date | NO | | Term start |
| end_date | date | NO | MUL | Term end (indexed for expiry queries) |
| is_renewable | tinyint(1) | NO | | Default: 1 |
| renewal_count | int unsigned | NO | | Default: 0 |
| last_renewed_at | timestamp | YES | | Last renewal date |
| status | varchar(50) | NO | MUL | Default: 'active' |
| notes | text | YES | | |
| is_archived | tinyint(1) | NO | | Soft delete flag, default 0 |
| archived_at | timestamp | YES | | |
| archived_by | bigint unsigned | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | On update cascade |
| created_by | bigint unsigned | YES | | FK to employees |
| updated_by | bigint unsigned | YES | | FK to employees |

### 3.2 Related Tables

| Table | Owner Module | Relationship |
|-------|--------------|--------------|
| members | Members | member_id FK, 1:many (multiple honorary grants over time) |

---

## 4. Routes

| Method | Path | Handler | Middleware | Permission |
|--------|------|---------|------------|------------|
| GET | /honorary | HonoraryController@index | auth | member.view |
| GET | /members/{memberId}/honorary/create | HonoraryController@create | auth | member.change_status |
| POST | /members/{memberId}/honorary | HonoraryController@store | auth, csrf | member.change_status |

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `honorary.created` | `{member_id, honorary_id}` | HonoraryController::store |

---

## 6. Events Consumed

None. This module does not listen to any events.

---

## 7. Cross-Module Dependencies

### 7.1 Honorary module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Rules | RuleEngine (HONORARY_DURATION_YEARS, HONORARY_RENEWABLE) |

### 7.2 Other modules that REFERENCE Honorary:

| Module | What it references |
|--------|-------------------|
| Members (AutoFreezeService) | `flagHonoraryExpiry()` — queries `honorary_members` to find expired terms, sets member status='expired' |
| Members (MembershipRulesService) | Knows about 'honorary' membership type |
| Members (bootstrap.php) | Registers the sidebar menu item for /honorary |
| Dashboard | DashboardDataService references honorary_members for stats |
| Archive | ArchiveService (cascade archive for honorary_members) |

---

## 8. Permissions

This module reuses Members permissions:

| Key | Usage |
|-----|-------|
| member.view | View honorary members list |
| member.change_status | Grant honorary membership (create/store) |

No dedicated permissions are registered in bootstrap.php.

---

## 9. Business Rules

### 9.1 Duration & Term
- Default duration: 1 year (configurable via `HONORARY_DURATION_YEARS`)
- Start date: user-specified or today
- End date: auto-calculated as `start_date + N years`

### 9.2 Renewability
- Controlled by `HONORARY_RENEWABLE` rule (default: true)
- `renewal_count` tracks how many times renewed
- `last_renewed_at` records most recent renewal timestamp

### 9.3 Member Type Validation & Activation
- As of 2026-06-11, `membership_type` is set at creation time — this module only validates type is 'honorary'
- `create()` and `store()` both guard: if type != 'honorary', redirect with error
- On successful store, the member is activated directly (status='active', membership number assigned)
- MembershipPaymentGuard::reconcile() bypasses honorary members entirely (no payment check)

### 9.4 No Fee
- Honorary memberships do not incur any fee — no PaymentService call
- BillingService returns form fee only (membership value = 0) for honorary type

---

## 10. Configuration Dependencies

| Rule Engine Key | Purpose |
|----------------|---------|
| HONORARY_DURATION_YEARS | Term length in years (default: 1) |
| HONORARY_RENEWABLE | Whether the membership can be renewed (default: true) |

---

## 11. Background Processes (Cron)

The Honorary module itself has no cron jobs, but is affected by:

**Members/AutoFreezeService::flagHonoraryExpiry()**:
- Queries `honorary_members` where `end_date < NOW()` and `status = 'active'`
- For each expired honorary, sets the parent member's status to 'expired'
- This is the only automated enforcement of honorary term limits

---

## 12. High-Risk Areas

### 12.1 Expiry Enforcement is External
- The Honorary module does NOT enforce its own expiry — it relies on Members/AutoFreezeService cron
- If cron fails or is not running, expired honorary members remain active indefinitely
- No webhook/event dispatch on expiry — the cron just mutates member status directly

### 12.2 No Renewal UI
- The model supports renewals (`renewal_count`, `last_renewed_at`, `is_renewable`)
- But there is NO controller endpoint or view for performing a renewal
- Renewals would currently need to be done directly in the database or via a future feature

### 12.3 Multiple Honorary Grants
- The model queries `ORDER BY start_date DESC LIMIT 1` for `getForMember()`
- A member could have multiple honorary_members records (historical grants)
- Only the latest one is considered "current"

---

## 13. Known Patterns & Gotchas

1. **Menu registered externally**: The Honorary sidebar menu item is registered by Members/bootstrap.php, not Honorary's own bootstrap.php
2. **Permission gate**: Creating honorary memberships requires `member.change_status` (not `member.create`) — this is a status change operation
3. **Index sorted by end_date ASC**: The list shows soonest-to-expire first, useful for renewal monitoring
4. **No validation on member state**: The controller does not check if the member is already honorary or if they have an active honorary record — multiple grants are technically possible
5. **Reason is required**: The only validation is that `reason` cannot be empty
