# Temporary Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Temporary module manages **temporary/auxiliary members** attached to working members. These are non-standard dependents who don't qualify as spouses or children. It handles:
- Adding temporary members across 8 categories (parent, special needs, unmarried daughter, sister, stepchild, orphan, disabled sibling, nanny)
- Category-specific validation (age limits, gender requirements, documentation)
- Fee calculation (10% of membership value, with championship exemption)
- Separation/independence eligibility per category
- Standalone index view (all temporary members across all members)
- Event-based injection into member profile via `member.profile_data` listener

It does **NOT** directly manage:
- Fee collection (Cashier/Payments modules process the payment_request)
- Annual subscription generation (Subscriptions module)
- Auto-expiry cron (Members/AutoFreezeService handles age-limit expiry)
- Member billing aggregation (Members/BillingService aggregates temp fees)
- Carnets/ID cards (Carnets module)

---

## 2. Directory & File Structure

```
app/Modules/Temporary/
├── bootstrap.php                          # Permission registration (4 permissions) + member.profile_data listener
├── Routes.php                             # Web routes (5 routes)
├── Controllers/
│   └── TemporaryController.php           # Main controller (index, CRUD)
├── Models/
│   └── TemporaryMember.php               # Main model (soft delete, events, NID dedup, categories)
├── Services/
│   └── TemporaryFeeCalculator.php        # Fee calc + category validation + separation logic
└── Views/
    ├── index.php                         # Standalone list of all temporary members (filterable)
    ├── create.php                        # Add temporary member form
    ├── show.php                          # Temporary member profile view
    └── _partials/
        └── temporary-table.php           # Embedded table in member profile
```

---

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

### 3.1 `temporary_members` Table (13 rows as of 2026-06-10)

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| member_id | bigint unsigned | NO | MUL | | FK to members |
| category | varchar(50) | NO | MUL | | One of 8 predefined categories |
| full_name_ar | varchar(200) | NO | | | Arabic name (required) |
| full_name_en | varchar(200) | YES | | | English name |
| national_id | varchar(14) | YES | MUL | | Egyptian NID |
| passport_number | varchar(50) | YES | | | For foreign members |
| date_of_birth | date | NO | | | Required |
| age_years | int unsigned | YES | | | Stored at creation (STATIC) |
| age_months | int unsigned | YES | | | Stored at creation (STATIC) |
| gender | varchar(10) | NO | | | 'male' or 'female' |
| nationality | varchar(100) | YES | | مصري | |
| relationship_to_member | varchar(100) | YES | | | Free-text relationship |
| has_championship | tinyint(1) | NO | | 0 | Championship exemption flag |
| disability_documentation | tinyint(1) | NO | | 0 | Disability docs provided |
| addition_fee | decimal(15,2) | NO | | 0.00 | Calculated fee |
| fee_breakdown_json | text | YES | | | JSON breakdown |
| fee_receipt_number | varchar(50) | YES | | | Receipt after payment |
| can_separate | tinyint(1) | NO | | 0 | Eligible for separation |
| can_get_independent | tinyint(1) | NO | | 0 | Eligible for independence |
| status | varchar(50) | NO | MUL | active | Lifecycle status |
| activated_by_payment_id | bigint unsigned | YES | | | FK to payments |
| join_date | date | YES | | | |
| photo_path | varchar(500) | YES | | | Uploaded photo |
| notes | text | YES | | | Free-text notes |
| is_archived | tinyint(1) | NO | MUL | 0 | Soft delete |
| archived_at | timestamp | YES | | | |
| archived_by | bigint unsigned | YES | | | FK to employees |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |
| updated_at | timestamp | NO | | CURRENT_TIMESTAMP | On update |
| created_by | bigint unsigned | YES | | | FK to employees |
| updated_by | bigint unsigned | YES | | | FK to employees |

**Current data distribution:**
- active / parent: 7
- active / nanny: 2
- active / unmarried_daughter: 1
- pending_payment / parent: 3

### 3.2 Category Values

| Category | Arabic Label | Notes |
|----------|-------------|-------|
| parent | والدين العضو العامل | No age limit |
| special_needs | ابناء ذوي الاحتياجات الخاصة | Must be 21+, needs disability docs |
| unmarried_daughter | بنات العضو غير المتزوجات | Female only |
| sister | شقيقة العضو العامل | Female only, age < SISTER_MAX_AGE (25) |
| stepchild | ابناء الزوج/الزوجة | Age < STEPCHILD_MAX_AGE (25) |
| orphan | الطفل اليتيم | Age < ORPHAN_MAX_AGE (25) |
| disabled_sibling | شقيق العضو المعاق | Needs disability docs |
| nanny | المربية | No special validation |

### 3.3 Status Values

| Status | Meaning |
|--------|---------|
| active | Currently active temporary member |
| inactive | Archived/removed |
| expired | Aged out (set by AutoFreezeService cron) |
| pending_payment | Awaiting fee payment |

### 3.4 Separation & Independence Flags

| Category | can_separate | can_get_independent |
|----------|-------------|-------------------|
| parent | No | Yes |
| special_needs | Yes | Yes |
| unmarried_daughter | Yes | Yes |
| sister | Yes | Yes |
| stepchild | Yes | Yes |
| orphan | No | No |
| disabled_sibling | No | No |
| nanny | No | No |

---

## 4. Routes

| Method | Path | Handler | Middleware | Permission |
|--------|------|---------|------------|------------|
| GET | /temporary | TemporaryController@index | auth | temp.view |
| GET | /members/{memberId}/temporary/create | TemporaryController@create | auth | temp.add |
| POST | /members/{memberId}/temporary | TemporaryController@store | auth, csrf | temp.add |
| GET | /members/{memberId}/temporary/{id} | TemporaryController@show | auth | temp.view |
| POST | /members/{memberId}/temporary/{id}/archive | TemporaryController@archive | auth, csrf | temp.remove |

**Note:** No edit route exists. Temporary members cannot be edited after creation.

---

## 5. Core Business Flows

### 5.1 Add Temporary Member Flow

```
1. GET /members/{memberId}/temporary/create
   - Validate member exists
   - Check lock: if membership_number assigned && not super_admin → blocked
   - Show form with category dropdown

2. POST /members/{memberId}/temporary (store)
   - Validate: name, category, DOB, gender, photo (all required)
   - Parse NID if provided → auto-fill DOB, age, gender
   - NID dedup check (members, spouses, children, temporary_members tables)
   - Self-check: NID cannot be same as member's NID
   - Category-specific validation via TemporaryFeeCalculator::validateCategory():
     * special_needs: age >= 21, disability_documentation required
     * unmarried_daughter: female only
     * sister: female only, age < SISTER_MAX_AGE
     * stepchild: age < STEPCHILD_MAX_AGE
     * orphan: age < ORPHAN_MAX_AGE
     * disabled_sibling: disability_documentation required
   - Photo required
   - Calculate fee via TemporaryFeeCalculator
   - Create temporary_member record
   - Set can_separate and can_get_independent based on category
   - Upload photo
   - Dispatch: temporary.added event
   - If fee > 0 AND member activated → payment_request to Cashier
   - If fee > 0 AND member NOT activated → included in aggregate bill
   - If exempt (championship) → active immediately with fee=0
```

### 5.2 Fee Calculation Logic (TemporaryFeeCalculator)

```
1. Load member record
2. Resolve CURRENT membership value from pricing_configs (same logic as Spouse/Child calculators):
   - If qualification_id set: SELECT price FROM pricing_configs WHERE branch_id + qual_id + membership_type + active + date-range
   - Else: cheapest active price for branch+type
   - Falls back to member.membership_value if no config found
   IMPORTANT: always uses CURRENT year pricing — not the stored historical membership_value
3. Check if on initial form (no membership_number = no extra form fee)
4. Check championship exemption:
   - If has_championship=true AND TEMP_CHAMPIONSHIP_EXEMPT rule says exempt=true → fee = 0
5. Otherwise:
   - TEMP_MEMBER_FEE rule → percentage (default 10%)
   - fee = currentMembershipValue * percentage / 100
6. Add form fee via FormFeeService::getFormFee() (0 if initial form, 570+sub if post-activation)
7. total_fee = fee + form_fee
8. Return: fee, percentage, membership_value (resolved current), form_fee, total_fee, breakdown
```

**Fix applied 2026-06-10:** Step 2 was previously missing — the calculator used `member.membership_value`
(historical/stored value) instead of the current pricing_configs price, causing incorrect fee amounts
for old members whose membership was created when the price was lower (e.g. 114,000 instead of 150,000 EGP).

### 5.3 Index View (Standalone)

```
GET /temporary
- Shows ALL temporary members across all members
- Filterable by: category, status, search query (name, NID, member name)
- Joins with members table for member_name and membership_number
- Limit 100 results
```

### 5.4 Archive (Remove) Flow

```
POST /members/{memberId}/temporary/{id}/archive
- Requires reason text
- Check lock (membership_number + super_admin)
- Sets: is_archived=1, archived_at, archived_by, status='inactive'
- Dispatches: temporary.removed event
```

### 5.5 Member Profile Data Injection

The bootstrap.php registers a `member.profile_data` event listener that:
- Queries `temporary_members WHERE member_id = ? AND is_archived = 0`
- Injects results into `$data['temporary']` for display in member profile

---

## 6. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `temporary.added` | `{member_id, temporary_id, category, fee}` | TemporaryController::store |
| `temporary.removed` | `{member_id, temporary_id, reason}` | TemporaryController::archive |

---

## 7. Events Consumed

| Event | Source Module | Action |
|-------|--------------|--------|
| `temporary.fee_paid` | Cashier | Activates temp member (sets status='active', activated_by_payment_id) |
| `member.profile_data` | Members | Injects temporary members data into member profile view |

---

## 8. Cross-Module Dependencies

### 8.1 Temporary module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Members | NationalIdParser (NID parsing), FormFeeService (form fee calculation) |
| Rules | RuleEngine (TEMP_MEMBER_FEE, TEMP_CHAMPIONSHIP_EXEMPT, SISTER_MAX_AGE, STEPCHILD_MAX_AGE, ORPHAN_MAX_AGE, SPOUSE_WORKING_AGE_THRESHOLD) |
| Cashier | PaymentRequestService (creates payment requests) |
| Shared | PhotoUploadService (photo upload/validation) |

### 8.2 Other modules that IMPORT FROM Temporary:

| Module | What it imports / references |
|--------|------------------------------|
| Members/BillingService | Aggregates temp member fees into member bill |
| Members/AutoFreezeService | Cron: expires age-limited categories at 25 |
| Members/MembershipPaymentGuard | Activates temp members on lump-sum payment |
| Members/RetroactiveMembershipService | Creates temp records during retroactive entry |
| Members/MemberController | Loads temp members for profile display, changelog |
| Cashier/bootstrap | Payment processing → temporary.fee_paid event |
| Payments/PaymentLifecycleService | Void/cancel of temp-related payments |
| Subscriptions/SubscriptionGenerator | Generates annual subscriptions for active temps |
| Death/DeathController | Transfers temp members to new primary member on death |
| Transfers/TransferProcessor | Moves temp members to new member on transfer |
| Waiver/WaiverController | Counts temp members for waiver eligibility |
| Archive/ArchiveService | Includes temp members in archive snapshot |
| Carnets/CarnetController | Issues carnets for temp members |
| Reports/ReportEngine | Temporary members report |
| Audit/AuditService | Labels for audit trail display |

---

## 9. Permissions

| Key | Description (AR) |
|-----|-----------------|
| temp.add | اضافة عضو مؤقت |
| temp.edit | تعديل عضو مؤقت |
| temp.remove | ازالة عضو مؤقت |
| temp.view | عرض الاعضاء المؤقتين |

**Note:** `temp.edit` permission is registered but no edit route exists. Reserved for future use.

**Super Admin gates:**
- Add/remove temporary members after member activation (membership_number assigned)

---

## 10. Background Processes (Cron)

Handled by `Members/Services/AutoFreezeService` (NOT in Temporary module):
- **freezeTemporaryAtAgeLimit()**: Temporary members in age-limited categories (sister, stepchild, orphan) who reach 25 → sets `status='expired'`
- Queries `temporary_members WHERE is_archived=0 AND status='active' AND category IN (age-limited categories)`
- Uses `TIMESTAMPDIFF(YEAR, date_of_birth, CURDATE()) >= 25`

---

## 11. Configuration Dependencies

| Rule Engine Key | Purpose | Default |
|----------------|---------|---------|
| TEMP_MEMBER_FEE | Percentage of membership value | 10% |
| TEMP_CHAMPIONSHIP_EXEMPT | Exempt if has championship | exempt=true |
| SISTER_MAX_AGE | Max age for sister category | 25 |
| STEPCHILD_MAX_AGE | Max age for stepchild category | 25 |
| ORPHAN_MAX_AGE | Max age for orphan category | 25 |
| SPOUSE_WORKING_AGE_THRESHOLD | Min age for special_needs category | 21 |

| Service Catalog | Purpose |
|----------------|---------|
| SVC_ADDITION_FORM | Form fee for addition (570 EGP) |
| SVC_ANNUAL_CHILD% (or temp-specific) | Annual subscription rate (~222 EGP) |

---

## 12. High-Risk Areas

### 12.1 No Edit Capability
- Once created, temporary members CANNOT be edited (no edit route or view)
- If data was entered incorrectly, only option is archive + re-create
- This means fee recalculation is impossible without deletion

### 12.2 Championship Exemption Logic
- `has_championship` flag sets fee to 0 — completely exempt
- No verification of championship claim (just a checkbox)
- If abused: members could avoid 10% fee entirely
- TEMP_CHAMPIONSHIP_EXEMPT rule can be toggled globally

### 12.3 Category Validation Gaps
- `parent` and `nanny` categories have NO validation beyond basic fields
- No max count per member for any category
- A member could theoretically add unlimited nannies or parents

### 12.4 Static Age Storage
- `age_years` stored at creation, never updated
- AutoFreezeService uses `TIMESTAMPDIFF` on `date_of_birth` for live age
- If display logic uses stored age_years → incorrect after years pass

### 12.5 Form Fee Inconsistency
- Uses `FormFeeService::getFormFee()` (includes annual sub) vs Children/Spouses which use `getFormFeeOnly()` + separate annual
- This means the total_fee for temporary members already includes annual subscription within form_fee
- Different decomposition pattern from sibling modules

### 12.6 NID Deduplication (Most Complete)
- Checks ALL 4 entity tables: members, spouses, children, temporary_members
- This is MORE complete than Children module (which doesn't check temporary_members)
- Inconsistency could allow a child NID that already exists as a temporary member

---

## 13. Known Patterns & Gotchas

1. **No edit route**: Only create, show, and archive operations exist
2. **Standalone index**: Only dependent module with its own `/temporary` index listing all records
3. **Profile data injection via EventBus**: Bootstrap listens to `member.profile_data` and injects temp data
4. **can_separate / can_get_independent**: Stored as columns, determined by category at creation time
5. **Category is permanent**: Cannot change category after creation
6. **Photo required**: Cannot add without uploading a photo
7. **Championship = full exemption**: No partial discount — either 10% or 0%
8. **Multiple parents allowed**: No max count for `parent` category (unlike spouses)
9. **Status managed externally**: AutoFreezeService sets 'expired', Cashier sets 'active' via payment

---

## 14. Fee Calculation Reference

| Scenario | Fee |
|----------|-----|
| Standard temp member | 10% of membership value + form fee |
| Championship exempt | 0 (completely free) |
| Form fee (post-activation) | Via FormFeeService::getFormFee() (includes annual sub) |
| On initial form | 10% only (no form fee) |

**Example:** Member with 150,000 EGP qualification:
- Standard: 15,000 + 570 (form) + 222 (annual) = 15,792 EGP
- Championship: 0 EGP

---

## Appendix: Files to Check Before Modifying Temporary

1. `app/Modules/Members/Services/BillingService.php` — Aggregates temp fees into member bill
2. `app/Modules/Members/Services/AutoFreezeService.php` — Cron expiry for age-limited categories
3. `app/Modules/Members/Services/MembershipPaymentGuard.php` — Activates temps on lump-sum payment
4. `app/Modules/Members/Services/FormFeeService.php` — Form fee + annual subscription logic
5. `app/Modules/Cashier/bootstrap.php` — Payment processing → temporary.fee_paid event
6. `app/Modules/Payments/Services/PaymentLifecycleService.php` — Void/cancel for temp payments
7. `app/Modules/Death/Controllers/DeathController.php` — Transfers temps on member death
8. `app/Modules/Transfers/Services/TransferProcessor.php` — Moves temps on member transfer
9. `app/Modules/Subscriptions/Services/SubscriptionGenerator.php` — Annual subscriptions for temps
10. `app/Modules/Reports/Services/ReportEngine.php` — Temporary members report
