# Foreign Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Foreign module manages **foreign (non-Egyptian) membership registration**. It:
- Records passport and nationality details for foreign members
- Calculates and processes foreign membership fees (in USD)
- Converts USD fees to EGP when exchange rate is provided
- Validates the member is already typed as 'foreign' (set at creation)
- Tracks passport expiry dates for compliance monitoring
- Validates passport minimum validity (configurable, default 6 months)

It does **NOT** directly manage:
- The core member record (Members module owns that)
- Payment processing (delegates to PaymentService)
- Annual subscriptions or renewals

---

## 2. Directory & File Structure

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

---

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

### 3.1 `foreign_member_details` 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 |
| passport_number | varchar(50) | NO | MUL | |
| issuing_country_id | bigint unsigned | NO | MUL | FK to countries |
| nationality_country_id | bigint unsigned | NO | MUL | FK to countries |
| passport_issue_date | date | NO | | |
| passport_expiry_date | date | NO | MUL | Indexed for expiry monitoring |
| place_of_birth | varchar(200) | YES | | |
| fee_amount_usd | decimal(15,2) | NO | | Base fee in USD |
| exchange_rate | decimal(15,4) | YES | | USD-to-EGP rate at time of payment |
| fee_amount_egp | decimal(15,2) | YES | | Computed: fee_usd * exchange_rate |
| fee_receipt_number | varchar(50) | YES | | |
| status | varchar(50) | NO | | 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:1 |
| countries | (lookup) | issuing_country_id, nationality_country_id FKs |
| payments | Payments | Created via PaymentService on registration |

---

## 4. Routes

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

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `foreign.registered` | `{member_id, foreign_id}` | ForeignController::store |

---

## 6. Events Consumed

None. This module does not listen to any events.

---

## 7. Cross-Module Dependencies

### 7.1 Foreign module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Rules | RuleEngine (FOREIGN_MEMBER_FEE_SHERATON, FOREIGN_MEMBER_FEE_CAPITAL, PASSPORT_MIN_VALIDITY_MONTHS) |
| Payments | PaymentService::processPayment() — processes membership_fee payment |

### 7.2 Other modules that REFERENCE Foreign:

| Module | What it references |
|--------|-------------------|
| Members | MembershipRulesService::getForeignMemberRules(), menu registration in bootstrap.php |
| Members | MembershipRulesService uses `isForeign` parameter in spouse fee calculations |
| Archive | ArchiveService (cascade archive for foreign_member_details) |
| Seasonal | SeasonalPricingService has 'foreign' nationality pricing path |
| Spouses | SpouseFeeCalculator (different first-spouse fee logic for foreign members) |

---

## 8. Permissions

This module reuses Members permissions:

| Key | Usage |
|-----|-------|
| member.view | View foreign members list |
| member.create | Create foreign membership records |

No dedicated permissions are registered in bootstrap.php.

---

## 9. Business Rules

### 9.1 Fee Calculation
- Fee is branch-dependent: Sheraton branch uses `FOREIGN_MEMBER_FEE_SHERATON`, New Capital uses `FOREIGN_MEMBER_FEE_CAPITAL`
- Default fee: $10,000 USD
- Optional EGP conversion: if exchange_rate provided, `fee_egp = fee_usd * exchange_rate`
- Payment processed via PaymentService with type='membership_fee', currency='EGP' or 'USD'

### 9.2 Passport Validation
- Passport must be valid for at least N months (default 6, configurable via `PASSPORT_MIN_VALIDITY_MONTHS`)
- `isPassportExpiringSoon()` helper method on model for compliance checks

### 9.3 Member Type Validation
- As of 2026-06-11, `membership_type` is set at creation time — this module only validates the member is already typed as 'foreign'
- `create()` and `store()` both guard: if type != 'foreign', redirect with error

---

## 10. Configuration Dependencies

| Rule Engine Key | Purpose |
|----------------|---------|
| FOREIGN_MEMBER_FEE_SHERATON | Fee in USD for Sheraton branch (default $10,000) |
| FOREIGN_MEMBER_FEE_CAPITAL | Fee in USD for New Capital branch |
| PASSPORT_MIN_VALIDITY_MONTHS | Minimum remaining passport validity (default 6) |

---

## 11. High-Risk Areas

### 11.1 Payment in Dual Currency
- Payment amount depends on whether exchange_rate was provided
- If EGP amount exists, payment is in EGP; otherwise USD
- PaymentService receives the amount/currency — downstream accounting must handle multi-currency

### 11.2 Activation via Cashier
- Foreign membership fees (`foreign_membership_fee` payment type) now trigger MembershipPaymentGuard::activateMember() via Cashier bootstrap
- The store method directly processes payment — Cashier post-payment handler activates the member
- Payment type `foreign_membership_fee` is recognized by MembershipPaymentGuard for reconciliation

### 11.3 No Renewal/Expiry Flow
- There is no renewal mechanism for foreign memberships
- Passport expiry is tracked but no automated action on expiry (unlike honorary members)

---

## 12. Known Patterns & Gotchas

1. **Menu registered externally**: The Foreign sidebar menu item is registered by Members/bootstrap.php, not Foreign's own bootstrap.php
2. **Soft delete via Model**: Uses `is_archived` column with standard Model soft delete pattern
3. **Countries table dependency**: Requires `countries` table with `name_ar` and `nationality_ar` columns
4. **No index view filtering**: The index shows ALL active foreign details sorted by passport expiry (soonest first) — useful for compliance monitoring
5. **Payment failure handling**: If payment processing fails after foreign_member_details creation, the record still exists but user gets a warning message
