# Spouses Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Spouses module manages **spouse dependents** attached to working members. It handles:
- Adding spouses to a member record (gender-enforced: male member → female spouses, female member → male spouse)
- Maximum spouse count enforcement (4 for male members, 1 for female members)
- Fee calculation based on spouse order, nationality, marriage date, and membership acquisition type
- Retroactive annual penalty fees for late additions (2nd+ spouses)
- "Acquired member" detection (transferred/divorced/death/waiver recipients pay different rates)
- Photo upload and NID validation with gender cross-check
- Spouse status lifecycle (active, pending_payment, inactive, divorced, deceased, transferred)

It does **NOT** directly manage:
- Fee collection (Cashier/Payments modules process the payment_request)
- Annual subscription generation (Subscriptions module)
- Divorce/transfer flows (Divorce, Death, Transfers modules manage lifecycle changes)
- Member billing aggregation (Members/BillingService aggregates spouse fees)
- Carnets/ID cards (Carnets module)

---

## 2. Directory & File Structure

```
app/Modules/Spouses/
├── bootstrap.php                          # Permission registration (4 permissions)
├── Routes.php                             # Web routes (7 routes)
├── Controllers/
│   └── SpouseController.php              # Main controller (CRUD, calculateFee API)
├── Models/
│   └── Spouse.php                        # Main model (soft delete, events, NID dedup, gender logic)
├── Services/
│   └── SpouseFeeCalculator.php           # Fee calculation engine (complex)
└── Views/
    ├── create.php                        # Add spouse form (NID, DOB, marriage date, photo)
    ├── edit.php                          # Edit spouse (limited fields)
    ├── show.php                          # Spouse profile view
    └── _partials/
        └── spouse-table.php              # Embedded table in member profile
```

---

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

### 3.1 `spouses` Table (81 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 |
| spouse_order | int unsigned | NO | | 1 | Sequential per member |
| 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 spouses |
| 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 | | | Enforced opposite of member |
| nationality | varchar(100) | YES | | مصري | Affects fee (foreign = higher) |
| religion | varchar(50) | YES | | | |
| qualification_id | bigint unsigned | YES | MUL | | FK to qualifications |
| occupation | varchar(200) | YES | | | |
| work_address | text | YES | | | |
| work_phone | varchar(20) | YES | | | |
| mobile | varchar(20) | YES | | | |
| marriage_date | date | NO | | | Required — used for retroactive penalty |
| join_date | date | NO | | | Date added to system |
| classification | varchar(30) | NO | | working | 'working' or 'dependent' |
| addition_fee | decimal(15,2) | NO | | 0.00 | Calculated total fee |
| fee_breakdown_json | text | YES | | | JSON breakdown |
| fee_receipt_number | varchar(50) | YES | | | Receipt after payment |
| status | varchar(50) | NO | MUL | active | Lifecycle status |
| activated_by_payment_id | bigint unsigned | YES | | | FK to payments |
| photo_path | varchar(500) | YES | | | Uploaded photo |
| 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 / working: 59
- pending_payment / working: 15
- divorced / working: 2
- transferred / working: 3
- transferred / dependent: 1
- inactive / working: 1

### 3.2 Classification Values

| Classification | Meaning |
|---------------|---------|
| working | Spouse aged 21+ (standard) |
| dependent | Spouse aged under 21 |
| seasonal | Seasonal membership (rare) |

### 3.3 Status Values

| Status | Meaning |
|--------|---------|
| active | Currently active spouse |
| pending_payment | Awaiting fee payment |
| inactive | Archived/removed |
| divorced | Divorced (set by Divorce module) |
| deceased | Deceased (set by Death module) |
| transferred | Transferred to own membership (Death/Divorce/Transfer) |

---

## 4. Routes

| Method | Path | Handler | Middleware | Permission |
|--------|------|---------|------------|------------|
| GET | /members/{memberId}/spouses/create | SpouseController@create | auth | spouse.add |
| POST | /members/{memberId}/spouses | SpouseController@store | auth, csrf | spouse.add |
| GET | /members/{memberId}/spouses/{id} | SpouseController@show | auth | spouse.view |
| GET | /members/{memberId}/spouses/{id}/edit | SpouseController@edit | auth | spouse.edit |
| POST | /members/{memberId}/spouses/{id} | SpouseController@update | auth, csrf | spouse.edit |
| POST | /members/{memberId}/spouses/{id}/archive | SpouseController@archive | auth, csrf | spouse.remove |
| POST | /api/spouses/calculate-fee | SpouseController@calculateFee | auth | spouse.add |

---

## 5. Core Business Flows

### 5.1 Add Spouse Flow

```
1. GET /members/{memberId}/spouses/create
   - Validate member exists
   - Check lock: if membership_number assigned && not super_admin → blocked
   - Check max count: male member max 4 (MAX_SPOUSES_MALE_MEMBER), female max 1 (MAX_SPOUSES_FEMALE_MEMBER)
   - Determine required gender (opposite of member)
   - Show form with spouse_order, qualifications, countries

2. POST /members/{memberId}/spouses (store)
   - Validate: name, DOB, gender, marriage_date, photo (all required)
   - Gender validation: must match required gender (opposite of member)
   - Parse NID if provided → auto-fill DOB, age, validate gender from NID
   - NID dedup check (members, spouses, children tables)
   - Self-check: NID cannot be same as member's NID
   - Min age check: SPOUSE_MIN_AGE (default 18)
   - Photo required and validated
   - Calculate fee via SpouseFeeCalculator
   - Create spouse record with classification (working if age>=21, dependent otherwise)
   - Upload photo
   - Dispatch: spouse.added event
   - If fee > 0 AND member activated → payment_request to Cashier
   - If fee > 0 AND member NOT activated → included in aggregate bill
   - If fee = 0 → spouse active immediately
```

### 5.2 Fee Calculation Logic (SpouseFeeCalculator)

```
1. Get member's current membership value from pricing_configs
2. Determine spouse order (existing active count + 1)
3. Check if on initial form (FormFeeService::isOnInitialForm)
4. Check if free slot (FormFeeService::isSpouseFreeSlot)
5. Determine: is acquired member? is foreign?

SPOUSE ORDER 1:
  - Free slot + not foreign + initial form → FREE (no fees at all)
  - Free slot + not foreign + late addition → form fee only
  - Foreign → SPOUSE_FOREIGN_FEE percentage
  - Acquired member (from transfer/divorce/death/waiver) → SPOUSE_ACQUIRED_MEMBER_FEE %
  - Base member (late addition) → SPOUSE_BASE_MEMBER_FEE %

SPOUSE ORDER 2: SPOUSE_2ND_FEE → percentage + annual_flat × years
SPOUSE ORDER 3: SPOUSE_3RD_FEE → percentage + annual_flat × years
SPOUSE ORDER 4+: SPOUSE_4TH_FEE → percentage + annual_flat × years

For 2nd+ spouses:
  - If on initial form: percentage only (no annual penalty)
  - If late addition: percentage + annual_flat × year_count
  - Year count = years from MAX(marriage_date, member_activated_at) to today
  - Partial year counts as full year

6. Add form fee (FormFeeService::getFormFeeOnly) if applicable
7. Add annual subscription if post-activation
8. Return: total_fee, breakdown, rule_applied
```

### 5.3 Acquired Member Detection

Checks these tables for completed transfers to the member:
- `transfer_requests.target_member_id`
- `divorce_cases.spouse_new_member_id`
- `death_cases.transferred_to_member_id`
- `waiver_requests.target_member_id`

If any match with `status='completed'` → member is "acquired" (higher 1st spouse fee).

### 5.4 Archive (Remove) Flow

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

---

## 6. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `spouse.added` | `{member_id, spouse_id, spouse_order, fee}` | SpouseController::store |
| `spouse.removed` | `{member_id, spouse_id, reason}` | SpouseController::archive |

---

## 7. Events Consumed

| Event | Source Module | Action |
|-------|--------------|--------|
| `spouse.fee_paid` | Cashier | Activates spouse (sets status='active', activated_by_payment_id) |

---

## 8. Cross-Module Dependencies

### 8.1 Spouses module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Members | NationalIdParser (NID parsing + gender validation), FormFeeService (form fee, free slot check) |
| Rules | RuleEngine (SPOUSE_MIN_AGE, SPOUSE_FOREIGN_FEE, SPOUSE_BASE_MEMBER_FEE, SPOUSE_ACQUIRED_MEMBER_FEE, SPOUSE_2ND/3RD/4TH_FEE, MAX_SPOUSES_MALE/FEMALE_MEMBER) |
| Cashier | PaymentRequestService (creates payment requests) |
| Shared | PhotoUploadService (photo upload/validation) |

### 8.2 Other modules that IMPORT FROM Spouses:

| Module | What it imports / references |
|--------|------------------------------|
| Members/BillingService | Aggregates spouse fees into member bill |
| Members/MembershipPaymentGuard | Activates spouses when member pays lump sum |
| Members/RetroactiveMembershipService | Creates spouse records during retroactive entry |
| Members/MemberController | Loads spouses for member profile display |
| Cashier/bootstrap | Payment processing → spouse.fee_paid event |
| Payments/PaymentLifecycleService | Void/cancel of spouse-related payments |
| Subscriptions/SubscriptionGenerator | Generates annual subscriptions for active spouses |
| Death/DeathController | Primary/secondary spouse selection, membership transfer on death |
| Divorce/DivorceController | Spouse selection for divorce case, status update to 'divorced' |
| Divorce/DivorceFeeCalculator | Reads spouse data for fee calculation |
| Transfers/TransferController | Spouse-based transfers, spouse count for target member |
| Transfers/TransferProcessor | Moves spouses to new member on transfer |
| Transfers/TransferEligibility | Checks spouse eligibility for transfer |
| Transfers/SeparationFeeCalculator | Extra spouse ordinal-position fee rules |
| Waiver/WaiverController | Counts spouses for waiver eligibility |
| Archive/ArchiveService | Includes spouses in member archive snapshot |
| Carnets/CarnetController | Issues carnets for spouses |

---

## 9. Permissions

| Key | Description (AR) |
|-----|-----------------|
| spouse.add | اضافة زوج/زوجة |
| spouse.edit | تعديل بيانات زوج |
| spouse.remove | ازالة زوج/زوجة |
| spouse.view | عرض بيانات الازواج |

**Super Admin gates:**
- Add/edit/remove spouses after member activation (membership_number assigned)
- Enforced via `isLocked()` + `isSuperAdmin()` in controller

---

## 10. Background Processes (Cron)

None owned by Spouses module directly. Related processes in other modules:
- **Subscriptions/SubscriptionGenerator**: Generates annual subscription lines for active spouses
- **Death/DeathController**: Transfers spouse to own membership on member death

---

## 11. Configuration Dependencies

| Rule Engine Key | Purpose | Default |
|----------------|---------|---------|
| SPOUSE_MIN_AGE | Minimum age for spouse | 18 |
| MAX_SPOUSES_MALE_MEMBER | Max spouses for male member | 4 |
| MAX_SPOUSES_FEMALE_MEMBER | Max spouses for female member | 1 |
| INITIAL_FREE_SPOUSES_COUNT | Number of free spouse slots | 1 |
| SPOUSE_BASE_MEMBER_FEE | 1st spouse late addition (base member) | percentage |
| SPOUSE_ACQUIRED_MEMBER_FEE | 1st spouse late addition (acquired member) | percentage |
| SPOUSE_FOREIGN_FEE | Foreign spouse fee | percentage |
| SPOUSE_2ND_FEE | 2nd spouse: percentage + annual_flat | 10% + 150/year |
| SPOUSE_3RD_FEE | 3rd spouse: percentage + annual_flat | 20% + 200/year |
| SPOUSE_4TH_FEE | 4th+ spouse: percentage + annual_flat | 30% + 300/year |
| SPOUSE_WORKING_AGE_THRESHOLD | Age threshold for 'working' classification | 21 |

| Service Catalog | Purpose |
|----------------|---------|
| SVC_ANNUAL_SPOUSE% | Annual subscription rate for spouses (~492 EGP) |
| SVC_ADDITION_FORM | Addition form fee (570 EGP) |

---

## 12. High-Risk Areas

### 12.1 Fee Calculation Complexity (HIGHEST RISK)
- 7+ fee paths depending on: order, is_initial, is_acquired, is_foreign, year_count
- Annual penalty uses `max(marriage_date, activated_at)` as start — partial year = full year
- "Acquired member" detection queries 4 different tables with try/catch
- If pricing_configs returns null → uses stored membership_value as fallback
- All money operations use bcmath — but branching logic is intricate

### 12.2 Gender Enforcement
- Member gender determines required spouse gender (opposite)
- NID-parsed gender is cross-checked against required gender
- If gender mismatch: explicit error shown
- Data still forces `$data['gender'] = $requiredGender` regardless of input (defense in depth)

### 12.3 Post-Activation Lock
- Same pattern as Children: once membership_number assigned, super_admin required
- But Divorce/Death/Transfer modules can change spouse status WITHOUT going through SpouseController
- Status can be set to 'divorced', 'transferred', 'deceased' by other modules directly

### 12.4 Spouse Order Integrity
- `spouse_order` is calculated at insertion time (count + 1)
- If a spouse is archived, the ordering of remaining spouses is NOT recalculated
- 2nd spouse removal doesn't make 3rd become 2nd — order is permanent
- Fee is based on order AT TIME OF ADDITION, not current count

### 12.5 Acquired Member Detection Fragility
- Checks 4 tables with `try/catch` and `tableExists()`
- If any table doesn't exist or column is renamed → silently returns false
- Member incorrectly classified as "base" → lower fee charged

---

## 13. Known Patterns & Gotchas

1. **Marriage date is REQUIRED**: Used for retroactive penalty calculation (2nd+ spouses)
2. **Pending calculation on initial form**: If member has no qualification yet, fee shows "pending" — recalculated when form is filled
3. **Classification is age-based**: working (21+) vs dependent (under 21) — set once at creation
4. **NID not strictly required**: Unlike children (required at 16), spouses can have passport_number instead
5. **Spouse status managed by MULTIPLE modules**: Spouses module only sets active/pending_payment/inactive; Divorce sets divorced; Death sets deceased/transferred; Transfers sets transferred
6. **Free slot logic**: `FormFeeService::isSpouseFreeSlot()` determines if this ordinal position is free — separate from order-based fee
7. **Qualifications stored on spouse**: Spouse has own qualification_id (separate from member's)
8. **Photo required at creation**: Cannot add spouse without photo

---

## 14. Fee Calculation Reference

| Scenario | Fee Formula |
|----------|-------------|
| 1st spouse, initial form, Egyptian | Free |
| 1st spouse, late addition, base member | SPOUSE_BASE_MEMBER_FEE % + form fee |
| 1st spouse, late addition, acquired member | SPOUSE_ACQUIRED_MEMBER_FEE % + form fee |
| 1st spouse, foreign | SPOUSE_FOREIGN_FEE % + form fee |
| 2nd spouse, initial form | 10% of membership value |
| 2nd spouse, late addition | 10% + (150 * years) + form fee + annual subscription |
| 3rd spouse, initial form | 20% of membership value |
| 3rd spouse, late addition | 20% + (200 * years) + form fee + annual subscription |
| 4th+ spouse, initial form | 30% of membership value |
| 4th+ spouse, late addition | 30% + (300 * years) + form fee + annual subscription |
| Form fee (post-activation) | 570 EGP |
| Annual subscription (post-activation) | ~492 EGP |

---

## Appendix: Files to Check Before Modifying Spouses

1. `app/Modules/Members/Services/BillingService.php` — Aggregates spouse fees into member bill
2. `app/Modules/Members/Services/MembershipPaymentGuard.php` — Activates spouses on lump-sum payment
3. `app/Modules/Members/Services/FormFeeService.php` — Free slot logic, form fee amounts
4. `app/Modules/Cashier/bootstrap.php` — Payment processing → spouse.fee_paid event
5. `app/Modules/Payments/Services/PaymentLifecycleService.php` — Void/cancel for spouse payments
6. `app/Modules/Death/Controllers/DeathController.php` — Spouse becomes new member on death
7. `app/Modules/Divorce/Controllers/DivorceController.php` — Spouse divorce flow
8. `app/Modules/Divorce/Services/DivorceFeeCalculator.php` — Divorce fee based on spouse data
9. `app/Modules/Transfers/Controllers/TransferController.php` — Spouse-based transfers
10. `app/Modules/Transfers/Services/TransferProcessor.php` — Moves spouses between members
11. `app/Modules/Subscriptions/Services/SubscriptionGenerator.php` — Annual subscriptions for spouses
