# Interviews Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Interviews module manages the **board of trustees interview process** for new membership applications. It handles:
- Scheduling interviews for members in the review/application phase
- Rescheduling with reason tracking and count
- Board decision recording (accepted/rejected) with notes
- Member status updates based on interview outcome
- Workflow engine integration (transitions interview workflow states)
- Linking interviews to form submissions

It does **NOT** directly manage:
- Member status lifecycle (but directly WRITES to members.status on decision)
- Payment processing (acceptance moves member to `payment_pending` — payments happen elsewhere)
- Notifications (no listeners registered for interview events currently)
- Calendar integration (dates are stored but no calendar/reminder system)

---

## 2. Directory & File Structure

```
app/Modules/Interviews/
├── bootstrap.php                          # Permission registration only (no menu — registered in Members)
├── Routes.php                             # Web routes (7 routes)
├── Controllers/
│   └── InterviewController.php           # Full controller (index, schedule, reschedule, decide, memberInterviews)
├── Models/
│   └── Interview.php                     # Model (search, getPending, getUpcoming, getForMember)
└── Views/
    ├── index.php                         # Interview list with search/filter
    ├── schedule.php                      # Schedule interview form
    └── decide.php                        # Decision form (accept/reject)
```

---

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

### 3.1 `interviews` Table (3 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 |
| form_submission_id | bigint unsigned | YES | MUL | FK to form_submissions |
| scheduled_date | date | NO | MUL | Interview date |
| scheduled_time | time | YES | | Interview time (optional) |
| location | varchar(200) | YES | | Interview location |
| rescheduled_from | date | YES | | Previous date before reschedule |
| reschedule_reason | text | YES | | Why it was rescheduled |
| reschedule_count | int unsigned | NO | | Default: 0 |
| decision | varchar(30) | NO | MUL | 'pending', 'accepted', 'rejected' |
| decision_notes | text | YES | | Board's notes on the decision |
| decision_date | timestamp | YES | | When the decision was made |
| board_member_names | text | YES | | Names of attending board members |
| board_decision_reference | varchar(100) | YES | | Reference number for the decision |
| notification_sent | tinyint(1) | NO | | Default: 0 |
| notification_sent_at | timestamp | YES | | When notification was sent |
| workflow_instance_id | bigint unsigned | YES | MUL | FK to workflow_instances |
| status | varchar(50) | NO | MUL | 'scheduled', 'completed_accepted', 'completed_rejected' |
| 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 |

**Current data distribution:**
- Total: 3 interviews
- Decision: all 'accepted'
- Status: all 'completed_accepted'

---

## 4. Routes (7 total)

| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /interviews | InterviewController@index | interview.view |
| GET | /interviews/schedule/{memberId} | InterviewController@schedule | interview.schedule |
| POST | /interviews/schedule/{memberId} | InterviewController@storeSchedule | interview.schedule |
| POST | /interviews/{id}/reschedule | InterviewController@reschedule | interview.schedule |
| GET | /interviews/{id}/decide | InterviewController@decide | interview.decide |
| POST | /interviews/{id}/decide | InterviewController@decide | interview.decide |
| GET | /interviews/member/{memberId} | InterviewController@memberInterviews | interview.view |

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `interview.scheduled` | `{interview_id, member_id, date, time}` | InterviewController::storeSchedule |
| `interview.rescheduled` | `{interview_id, member_id, old_date, new_date, reason}` | InterviewController::reschedule |
| `interview.decided` | `{interview_id, member_id, decision, notes}` | InterviewController::decide |

---

## 6. Events Consumed (by other modules)

**No modules currently listen to interview events.**

The events are dispatched but have no registered listeners. This means:
- No notifications are sent when interviews are scheduled or decided
- No audit trail entries are created via events (though the `updated_by` column provides basic tracking)
- No accounting impact

---

## 7. Cross-Module Dependencies

### 7.1 Interviews module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Members | members table (member lookup, status check, form_submission_id) |
| Workflow | WorkflowEngine::transition (schedule_interview, accept, reject transitions) |
| Forms | References form_submission_id (data only, no service import) |

### 7.2 Other modules that IMPORT FROM Interviews:

| Module | What it references |
|--------|-------------------|
| Members | References 'interview_scheduled' status in views and models; sidebar menu links to /interviews |
| Members | MembershipValidationService references interview status |
| Members | FormFeeService may check interview state |
| Dashboard | DashboardDataService shows interview-related stats |
| Tutorials | Tutorial content about interview process |

### 7.3 Tight Coupling with Members

The Interviews module **directly writes to the members table** in `decide()`:
```php
$db->update('members', ['status' => $newMemberStatus, 'updated_at' => ...], '`id` = ?', [...]);
```
- On acceptance: sets member status to `payment_pending`
- On rejection: sets member status to `rejected`

This bypasses the Members module's own status management and any potential event dispatching for status changes.

---

## 8. Permissions

| Key | Description |
|-----|-------------|
| interview.view | View interview list and history |
| interview.schedule | Schedule and reschedule interviews |
| interview.decide | Record accept/reject decisions |
| interview.conduct | Conduct an interview (registered but not used in routes) |

---

## 9. Core Business Flows

### 9.1 Member Application Flow (Interview Phase)

```
Member lifecycle context:
  potential → under_review → interview_scheduled → accepted/rejected → payment_pending → active
                                    ↑                      ↑
                            (this module)          (this module)
```

### 9.2 Schedule Interview

```
1. Validate member exists and is not archived
2. Check for existing pending interview (displayed but doesn't block scheduling)
3. Validate:
   - scheduled_date is required
   - scheduled_date must not be in the past
4. Create interview record (decision='pending', status='scheduled')
5. If member has workflow_instance_id:
   - WorkflowEngine::transition('schedule_interview') — wrapped in try/catch
6. Dispatch interview.scheduled event
```

### 9.3 Reschedule Interview

```
1. Find interview with decision='pending'
2. Validate new date and reason are provided
3. Update: rescheduled_from=old_date, new date, increment reschedule_count
4. Dispatch interview.rescheduled event
```

### 9.4 Decide Interview (Board Decision)

```
1. Find interview (joined with member data)
2. GET: Show decision form with interview details
3. POST: Validate decision is 'accepted' or 'rejected'
4. Update interview:
   - decision, decision_notes, decision_date
   - board_member_names, board_decision_reference
   - status = 'completed_accepted' or 'completed_rejected'
5. Update member status directly:
   - accepted → member.status = 'payment_pending'
   - rejected → member.status = 'rejected'
6. If member has workflow_instance_id:
   - WorkflowEngine::transition('accept' or 'reject')
7. Dispatch interview.decided event
```

---

## 10. High-Risk Areas

### 10.1 Direct Member Status Write (CRITICAL)
- Interviews module directly updates `members.status` column
- This bypasses Members module's own status change logic
- No `member.status_changed` event is dispatched from here
- If Members module adds status change validation or side effects, this module won't trigger them
- Could lead to inconsistent state if other code expects status changes to go through a central service

### 10.2 WorkflowEngine Failure is Silent
- Workflow transitions are wrapped in try/catch with only a Logger::warning
- If the workflow service is down or misconfigured, the interview proceeds without workflow state updates
- This can leave workflow instances in stale states

### 10.3 No Duplicate Prevention
- Nothing prevents scheduling multiple pending interviews for the same member
- `getPendingForMember()` is called on the schedule page for display, but doesn't block creation
- Multiple pending interviews could lead to confusion about which one is active

### 10.4 No Date Validation Beyond Past Check
- Only validates `scheduled_date < today` (rejects past dates)
- No check for weekends, holidays, or conflicting interviews
- No maximum future date limit

### 10.5 Notification Fields Unused
- `notification_sent` and `notification_sent_at` columns exist
- No code in the module ever sets these to true
- No notification module listener registered for interview events
- Members scheduled for interviews receive no automated notification

---

## 11. Known Patterns & Gotchas

1. **No soft delete**: Interviews are never deleted — no `is_archived` column
2. **Menu lives in Members**: The Interviews menu item is registered in Members/bootstrap.php, not here
3. **No standalone access**: Every interview is tied to a member — no interview management without member context
4. **Reschedule preserves only last**: `rescheduled_from` stores only the immediately previous date, not full history
5. **Form submission link**: Interviews optionally link to `form_submission_id` (from the member's form), providing context for the board
6. **Decision is final**: No mechanism to change a decision once recorded — would require direct DB edit
7. **Status field is derived**: `status` column is always set based on `decision` — it's denormalized for easier querying but could drift
8. **Dashboard integration**: Dashboard shows upcoming interviews via `Interview::getUpcoming(7)` — next 7 days with pending decisions
