# Members Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Members module is the **core entity module** of the ERP. It manages:
- Working member registration (from potential → active lifecycle)
- Member profile data (personal, employment, financial)
- Membership form flow (form fee → fill form → pay membership → activation)
- Member billing calculation (aggregates all fees: membership + dependents)
- Membership number assignment (sequential, payment-order-based)
- Status lifecycle management (13 possible states)
- Retroactive member entry (bulk historical data import)
- Membership validation (for external systems like access control)
- Reports (age, children aging out, transfers, debts, subscriptions)

It does **NOT** directly manage:
- Dependents (Spouses, Children, Temporary modules own those)
- Payments (Payments/Cashier modules handle money)
- Subscriptions (Subscriptions module)
- Transfers/Divorce/Death (separate modules)
- Installments (Installments module)

---

## 2. Directory & File Structure

```
app/Modules/Members/
├── bootstrap.php                          # Menu + permission registration
├── Routes.php                             # Web routes (36 routes)
├── ApiRoutes.php                          # REST API v1 (3 endpoints)
├── Controllers/
│   ├── MemberController.php              # Main controller (index, CRUD, payments, status)
│   ├── MemberApiController.php           # Internal API (NID parse, search)
│   ├── ReportController.php              # 7 report endpoints
│   ├── RetroactiveWizardController.php   # Bulk retroactive entry
│   └── Api/
│       └── MemberApiV1Controller.php     # External REST API
├── Models/
│   ├── Member.php                        # Main model (soft delete, events)
│   └── MemberNote.php                    # Simple notes model
├── Services/
│   ├── AutoFreezeService.php             # Cron: freeze children@25, block unpaid
│   ├── BillingService.php                # Calculate full member bill
│   ├── BoardOfferService.php             # Time-limited board discount offers
│   ├── FormFeeService.php                # Pre/post-activation form fee logic
│   ├── FormNumberGenerator.php           # (alias — see MemberNumberGenerator)
│   ├── MemberNumberGenerator.php         # Form number + membership number sequencing
│   ├── MemberSearchService.php           # Full-text search across member fields
│   ├── MembershipPaymentGuard.php        # THE authority for activation/deactivation
│   ├── MembershipRulesService.php        # All business rules (fees, transfers, violations)
│   ├── MembershipValidationService.php   # Check member validity for external systems
│   ├── NationalIdParser.php              # Egyptian 14-digit NID parser
│   ├── RetroactiveMembershipService.php  # Historical member + payments creation
│   └── WaiverService.php                 # Membership waiver (transfer to another person)
└── Views/
    ├── index.php                         # Paginated member list with filters
    ├── create.php                        # New member form (minimal: name, NID, phone, branch)
    ├── show.php                          # Full member profile (tabs: family, financial, docs)
    ├── edit.php                          # Edit form (limited post-activation unless super admin)
    ├── fill-form.php                     # Detailed membership application form
    ├── search.php                        # Search results page
    ├── changelog.php                     # Audit trail viewer
    ├── retroactive-wizard.php            # Bulk import wizard
    ├── _partials/
    │   ├── profile-header.php            # Member info header component
    │   ├── family-tab.php                # Spouses + children + temp members
    │   ├── financial-summary.php         # Bill breakdown + payment actions
    │   ├── documents-tab.php             # Uploaded documents
    │   └── activity-timeline.php         # Timeline of events
    └── reports/
        ├── index.php                     # Report dashboard
        ├── age-report.php                # Member age distribution
        ├── children-aging.php            # Children nearing age 25
        ├── subscription-status.php       # Annual subscription payment status
        ├── transfers.php                 # Transfer history
        ├── unpaid-debts.php              # Outstanding debts
        └── waivers.php                   # Waiver request log
```

---

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

### 3.1 `members` Table (76 rows as of 2026-06-10)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| membership_number | varchar(20) | YES | UNI | Assigned on payment (null = pre-activation) |
| activated_by_payment_id | bigint unsigned | YES | | FK to payments |
| activated_at | datetime | YES | | When membership was activated |
| form_number | varchar(50) | YES | MUL | Sequential form number |
| form_date | date | YES | | Date form was issued |
| form_submission_id | bigint unsigned | YES | MUL | FK to form_submissions |
| branch_id | bigint unsigned | NO | MUL | FK to branches |
| membership_type | varchar(50) | NO | MUL | Default: 'working' |
| member_category | varchar(50) | NO | | Default: 'working_member' |
| status | varchar(50) | NO | MUL | Default: 'potential' |
| full_name_ar | varchar(200) | NO | MUL | Arabic full name |
| full_name_en | varchar(200) | YES | | English full name |
| national_id | varchar(14) | YES | UNI | Egyptian 14-digit NID |
| passport_number | varchar(50) | YES | MUL | For foreign members |
| id_type | varchar(20) | NO | | 'national_id' or 'passport' |
| id_issue_date | date | YES | | |
| id_expiry_date | date | YES | | |
| date_of_birth | date | NO | | Parsed from NID or manual |
| 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' |
| place_of_birth | varchar(200) | YES | | |
| nationality | varchar(100) | YES | | Default: 'مصري' |
| governorate_code | varchar(2) | YES | | From NID positions 8-9 |
| religion | varchar(50) | YES | | |
| qualification_id | bigint unsigned | YES | MUL | FK to qualifications |
| marital_status | varchar(30) | YES | | |
| phone_home | varchar(20) | YES | | |
| phone_mobile | varchar(20) | NO | MUL | Primary contact |
| phone_international | varchar(30) | YES | | |
| email | varchar(255) | YES | | |
| emergency_name | varchar(200) | YES | | |
| emergency_phone | varchar(20) | YES | | |
| residence_type | varchar(30) | YES | | |
| residence_address | text | YES | | |
| landmark | varchar(200) | YES | | |
| floor | varchar(20) | YES | | |
| apartment | varchar(20) | YES | | |
| area | varchar(100) | YES | | |
| governorate | varchar(100) | YES | | |
| correspondence_address | varchar(50) | YES | | |
| employment_type | varchar(50) | YES | | |
| occupation | varchar(200) | YES | | |
| job_title | varchar(200) | YES | | |
| employment_date | date | YES | | |
| business_address | text | YES | | |
| office_phone | varchar(20) | YES | | |
| office_fax | varchar(20) | YES | | |
| business_activity | varchar(200) | YES | | |
| membership_value | decimal(15,2) | YES | | Based on qualification |
| credit_limit | decimal(15,2) | NO | | Default: 0.00 |
| credit_balance | decimal(15,2) | NO | | Default: 0.00 |
| special_discount_id | bigint unsigned | YES | MUL | FK to special_discounts |
| special_discount_document | varchar(500) | YES | | File path |
| discount_amount | decimal(15,2) | YES | | Computed from percentage |
| payment_method | varchar(30) | YES | | cash/check/installment |
| referral_source | varchar(100) | YES | | |
| referred_by_representative_id | int unsigned | YES | MUL | FK to sales_representatives |
| photo_path | varchar(500) | YES | | Uploaded photo path |
| workflow_instance_id | bigint unsigned | YES | MUL | FK to workflow_instances |
| 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 |
| is_archived | tinyint(1) | NO | MUL | Soft delete flag |
| archived_at | timestamp | YES | | |
| archived_by | bigint unsigned | YES | | |
| transferred_from_divorce_id | bigint unsigned | YES | | FK to divorce_cases |
| original_membership_number | varchar(20) | YES | | Previous number before transfer |

**Current data distribution (status):**
- active: 66
- deceased: 3
- under_review: 3
- payment_pending: 1
- pending_cheques: 1
- potential: 1
- transferred: 1

### 3.2 `member_notes` Table

| Column | Type | Notes |
|--------|------|-------|
| id | bigint unsigned | PRI, auto_increment |
| member_id | bigint unsigned | FK to members, indexed |
| note_text | text | |
| created_at | timestamp | |
| created_by | bigint unsigned | FK to employees |

### 3.3 Related Tables (owned by other modules but critical to Members)

| Table | Owner Module | Relationship |
|-------|--------------|--------------|
| spouses | Spouses | member_id FK, 1:many |
| children | Children | member_id FK, 1:many |
| temporary_members | Temporary | member_id FK, 1:many |
| payments | Payments | member_id FK, 1:many |
| payment_requests | Cashier | member_id FK, 1:many |
| receipts | Receipts | member_id FK, 1:many |
| subscriptions | Subscriptions | member_id FK, 1:many per year |
| installment_plans | Installments | member_id FK, 0:many |
| installment_schedule | Installments | via installment_plans |
| installment_cheques | Installments | via installment_plans |
| fines | Fines | member_id FK, 0:many |
| violations | Fines | member_id FK, 0:many |
| transfer_requests | Transfers | source_member_id FK |
| divorce_cases | Divorce | original_member_id / transferred_to_member_id |
| death_cases | Death | member_id FK |
| audit_trail | Audit | entity_type='members', entity_id |
| carnets | Carnets | member_id FK |
| documents | Documents | entity_type='members', entity_id |
| honorary_members | Honorary | member_id FK |
| pricing_configs | Pricing | branch_id + qualification_id |
| special_discounts | Pricing | referenced by special_discount_id |
| board_offers | (Settings?) | Time-bound discount offers |
| qualifications | (lookup) | Referenced by qualification_id |
| branches | Branches | Referenced by branch_id |
| governorates | (lookup) | Referenced by governorate_code |
| sales_representatives | (Sales?) | Referenced by referred_by_representative_id |
| workflow_instances | Workflow | Referenced by workflow_instance_id |
| form_submissions | Forms | Referenced by form_submission_id |
| subscription_rate_overrides | Subscriptions | Per-type subscription exemption/rates |
| system_config | Settings | membership.form_start_number, membership.number_start |

---

## 4. Member Status Lifecycle

```
potential → under_review → interview_scheduled → accepted → payment_pending → active
                                                                ↓
                                                         pending_cheques → active
                                                                
active → frozen / suspended / dropped / expired / terminated
rejected (from under_review or interview_scheduled)
```

**Status values:** potential, under_review, interview_scheduled, accepted, rejected, payment_pending, pending_cheques, active, frozen, suspended, dropped, expired, terminated

**Key invariant (MembershipPaymentGuard):** A member is "active" **if and only if** they have at least one non-voided payment of a valid membership payment type (`membership_fee`, `down_payment`, `foreign_membership_fee`, `sports_membership_fee`, `seasonal_fee`). Exception: `honorary` members activate via board decision (no payment required).

---

## 4B. Membership Type System (Primary Types)

As of 2026-06-11, `membership_type` is a **primary selection at creation time**, not a post-creation conversion. All 5 types are selectable from the member creation form.

| Type | Label (AR) | Min Age | Allows Dependents | Requires Qualification | Fee Logic |
|------|-----------|---------|-------------------|----------------------|-----------|
| working | عضو عامل | 21 | YES | YES | Qualification-based pricing (150K/225K/300K) |
| seasonal | عضوية موسمية | 0 | NO | NO | Duration-based via SeasonalPricingService |
| sports | عضوية رياضية | 0 | NO | NO | 50% conversion fee of working membership value |
| honorary | عضوية شرفية | 0 | NO | NO | Form fee only, membership value = 0, no payment needed |
| foreign | عضوية أجنبية | 21 | YES | NO | $10,000 USD (Sheraton) / $15,000 USD (New Capital) from RuleEngine |

**Key behaviors:**
- `MembershipRulesService::getCreatableMembershipTypes()` returns all 5 types
- `MembershipRulesService::allowsDependents(type)` — `working` and `foreign` return true
- Spouse/Children/Temporary controllers guard against types where `allowsDependents()` is false
- `BillingService::getMemberBill()` dispatches via `match($membershipType)` to type-specific billing methods
- `MembershipPaymentGuard::reconcile()` bypasses honorary (no payment required)
- Special module detail pages (Foreign/Honorary/Sports/Seasonal) validate member type before allowing access
- Death/Divorce transfers inherit `membership_type` from original member
- `subscription_rate_overrides` table controls per-type subscription exemption/rates

**Subscription exemption:**
- Honorary and Seasonal types are exempt from annual subscriptions (controlled via `subscription_rate_overrides.is_exempt`)
- Sports and Foreign follow same subscription rates as working

---

## 5. Core Business Flows

### 5.1 New Member Registration Flow

**Working member flow (full lifecycle):**
```
1. POST /members (store) → status='potential', form_number assigned
2. POST /members/{id}/pay-form-fee → payment_request(form_fee, 505 EGP) → Cashier
3. GET /members/{id}/fill-form → sets qualification/membership_value → status='under_review'
4. Interview/Acceptance process → status='accepted' or 'payment_pending'
5. POST /members/{id}/pay-membership → payment_request(membership_fee/down_payment)
6. Cashier completes → MembershipPaymentGuard::activateMember() → status='active'
```

**Non-working type flows (skip acceptance, stay 'potential' until payment):**
```
Foreign: potential → form_fee → (optional: /foreign/create for passport+exchange rate) → pay-membership(foreign_membership_fee) → active
Sports:  potential → form_fee → (optional: /sports/create for discipline info) → pay-membership(sports_membership_fee) → active
Seasonal: potential → form_fee → /seasonal/create (auto-sends payment_request) → Cashier → active
Honorary: potential → form_fee → /honorary/create → immediately active (no payment)
```

**Key:** Non-working types have `$formFilled = true` unconditionally (MemberController::show, line 221). The show page payment section allows `potential` status for non-working types with form_fee_paid.

### 5.2 Billing Calculation (BillingService::getMemberBill)

Dispatches by `membership_type` via PHP `match()`:
- **working** → `getWorkingBill()`: Full aggregation (form fee + membership value + dependents + discounts)
- **honorary** → `getHonoraryBill()`: Form fee only, membership value = 0
- **foreign** → `getForeignBill()`: Form fee + $10K USD from RuleEngine
- **seasonal** → `getSeasonalBill()`: Form fee + duration-based fee from `seasonal_memberships` record
- **sports** → `getSportsBill()`: Form fee + 50% conversion fee

Working member bill aggregates:
1. Form fee (505 EGP = 500 form + 5 martyrs stamp)
2. Membership value (qualification-based: 150K/225K/300K)
3. Special discount (percentage off membership value)
4. Spouse fees (1st free, 2nd=10%, 3rd=20%, 4th+=30% of membership value)
5. Children fees (first 2 under 18 free, others by age bracket)
6. Temporary member fees (10% of membership value)
7. Board offer discount (time-limited cash discount)
8. Tracks: paid, in_queue, pending for each line item

### 5.3 Retroactive Entry (super admin only)

Creates a complete historical member with:
- Member record (status='active' immediately)
- Form fee payment
- Membership fee payment (cash/check/installment with full schedule)
- Spouses, children, temporary members
- Historical subscriptions
- Violations and fines
- All with backdated timestamps

---

## 6. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `member.created` | `{member_id, form_number}` or `{member_id, retroactive: true}` | MemberController::store, RetroactiveMembershipService |
| `member.status_changed` | `{member_id, old, new}` | MemberController::changeStatus |
| `member.activated` | `{member_id, membership_number}` | Cashier bootstrap, PaymentController, ChequeService |
| `member.pending_cheques` | `{member_id}` | Cashier bootstrap |
| `member.dropped` | `{member_id, reason}` | DefaultChecker, OverdueFineApplicator |
| `member.permanently_dropped` | `{member_id}` | OverdueFineApplicator |
| `member.profile_data` | `{&data}` (mutable) | Used by Payments, Temporary, Documents, Seasonal (injects extra tabs) |

---

## 7. Events Consumed (by other modules)

| Event | Listener Module | Action |
|-------|----------------|--------|
| `member.activated` | Sales | Unknown (check Sales/bootstrap.php) |
| `member.dropped` | Accounting | Journal entry for membership write-off |
| `member.profile_data` | Payments | Injects payment history tab data |
| `member.profile_data` | Temporary | Injects temporary members tab |
| `member.profile_data` | Documents | Injects documents tab |
| `member.profile_data` | Seasonal | Injects seasonal membership data |

---

## 8. Cross-Module Dependencies

### 8.1 Members module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Payments | PaymentService (referenced in imports but not directly called in main flows) |
| Cashier | PaymentRequestService (creates payment requests for treasury) |
| Rules | RuleEngine (WORKING_MEMBER_MIN_AGE, spouse/child fee rules) |
| Pricing | SpecialDiscount model (discount lookups) |
| Workflow | WorkflowEngine (new membership workflow, status transitions) |
| Forms | FormBridge (submits filled form data to Forms module) |
| ServiceCatalog | ServicePrice (form fee amount lookup) |
| Shared | PhotoUploadService (photo upload handling) |

### 8.2 Other modules that IMPORT FROM Members:

| Module | What it imports |
|--------|----------------|
| Children | MemberNumberGenerator, MembershipRulesService, FormFeeService |
| Spouses | MemberNumberGenerator, MembershipRulesService, FormFeeService |
| Temporary | MemberNumberGenerator, MembershipRulesService, FormFeeService |
| Coaches | Member model (coach can be linked to member) |
| Death | Member model, MembershipPaymentGuard |
| Divorce | Member model, MembershipPaymentGuard, MembershipRulesService |
| Installments | MembershipPaymentGuard, ChequeService activates members |
| Payments | MembershipPaymentGuard (activation/deactivation on payment) |
| PlayerAffairs | Member model, MembershipValidationService |
| SportsActivity | MembershipValidationService (access control) |
| Transfers | Member model, MembershipRulesService |
| HR | Member model (employee-member link) |

---

## 9. Permissions

| Key | Description |
|-----|-------------|
| member.view | View member list and profiles |
| member.create | Create new members |
| member.edit | Edit member details |
| member.archive | Archive (soft-delete) members |
| member.search | Use search functionality |
| member.view_financial | View financial data |
| member.change_status | Manually change member status |
| member.pay_form_fee | Initiate form fee payment |
| member.pay_membership | Initiate membership payment |
| member.fill_form | Fill the detailed membership form |
| member.reports | Access membership reports |

**Super Admin gates:**
- Retroactive wizard access
- Edit after activation (normally locked once membership_number assigned)
- Edit national_id and full_name_ar

---

## 10. Background Processes (Cron)

`AutoFreezeService` handles scheduled tasks:
- **processAutoFreeze()**: Male children who reach age 25 → status='separated', classification='separated'
- **freezeTemporaryAtAgeLimit()**: Temporary members in age-limited categories → status='expired' at 25
- **flagHonoraryExpiry()**: Honorary members past their 1-year term → member status='expired'
- **getChildrenNearingThreshold()**: Report children within 6 months of 25 (not a mutation, reporting only)
- **checkSubscriptionBlock()**: Checks if annual subscription is unpaid (used on-demand, not cron)

---

## 11. Configuration Dependencies

| Config Key (system_config) | Purpose |
|---------------------------|---------|
| membership.form_start_number | Starting form number for sequencing |
| membership.number_start | Starting membership number for sequencing |

| Rule Engine Keys | Purpose |
|-----------------|---------|
| WORKING_MEMBER_MIN_AGE | Minimum age for working membership (default: 21) |
| INITIAL_FREE_SPOUSES_COUNT | How many spouses included free (default: 1) |
| INITIAL_FREE_CHILDREN_COUNT | How many children included free |
| CHILD_INCLUDED_MAX_AGE | Max age for free children |
| SPOUSE_2ND_FEE / SPOUSE_3RD_FEE / SPOUSE_4TH_FEE | Spouse addition fee percentages |
| FORM_ADDITION_FEE | Fee for adding dependents post-activation (570) |
| NEW_NUMBERING_START_DATE | New 2/XXXX numbering format config |

| Service Catalog Codes | Purpose |
|----------------------|---------|
| SVC_NEW_FORM | Form fee amount (505.00) |
| SVC_ADDITION_FORM | Addition form fee (570.00) |
| SVC_ANNUAL_CHILD% | Child annual subscription rate |
| SVC_MEMBERSHIP_{QUAL_CODE} | Membership value by qualification |

---

## 12. High-Risk Areas

### 12.1 MembershipPaymentGuard (CRITICAL)
- **Single source of truth** for member activation/deactivation
- Called from Cashier, Payments, Installments modules
- If modified incorrectly: members get activated without payment or deactivated with valid payments
- `reconcile()` is called on every member profile view as a safety net

### 12.2 BillingService
- Complex aggregation across 5 entity types (members, spouses, children, temps, payments)
- Uses bcmath for all money calculations
- Multiple fallback paths (try/catch around dependent queries)
- If billing is wrong: members pay incorrect amounts

### 12.3 Form Number / Membership Number Sequencing
- Sequential numbers based on MAX() queries — race conditions possible under concurrent use
- Form numbers assigned at creation, membership numbers at payment confirmation
- If sequencing breaks: duplicate or skipped numbers (compliance issue)

### 12.4 Status Transitions
- Multiple code paths can change status (MemberController, PaymentGuard, Cashier, Installments, Subscriptions)
- WorkflowEngine is optional (try/catch) — won't block transitions if workflow is misconfigured
- No single state machine enforcement — status changes are scattered

### 12.5 Retroactive Entry
- Bypasses all normal validation and workflow
- Creates payments, receipts, installments, subscriptions with backdated timestamps
- Super admin only — but if data is wrong, very hard to untangle

---

## 13. Known Patterns & Gotchas

1. **Post-activation lock**: Once `membership_number` is assigned, most fields are read-only for non-super-admin users
2. **Reconciliation on view**: `MembershipPaymentGuard::reconcile()` runs on every `/members/{id}` page load — it can change member status silently
3. **Payment cancellation cascade**: Paying membership fee auto-cancels pending individual addition_fee requests
4. **Dependent activation via parent**: When member pays lump sum, all pending dependents without separate payment_requests get activated by `activateIncludedDependents()`
5. **Death/divorce transfers**: Members can be created from these flows with `transferred_from_divorce_id` set and `original_membership_number` preserved
6. **age_years/age_months are STATIC**: Stored at creation time, not recomputed. Children use TIMESTAMPDIFF in queries for live age
7. **Multiple activation sources recognized by reconcile()**: membership_fee, down_payment, foreign_membership_fee, sports_membership_fee, seasonal_fee, death_fee, and any valid activated_by_payment_id. Honorary members bypass reconcile entirely (no payment required).

---

## 14. API Endpoints

### Internal API (web session auth)
- `POST /api/members/parse-nid` — Parse national ID, return DOB/gender/governorate
- `GET /api/members/search` — JSON search results
- `POST /api/members/search` — JSON search results (POST variant)

### External API v1 (CORS + api_auth middleware)
- `GET /api/v1/members` — List members (paginated)
- `GET /api/v1/members/search` — Search members
- `GET /api/v1/members/{id}` — Get single member

---

## 15. Membership Pricing Reference

| Qualification | Price (since 2024-07-01) |
|--------------|--------------------------|
| High (عالى) | 150,000 EGP |
| Medium (متوسط) | 225,000 EGP |
| None (بدون) | 300,000 EGP |

| Spouse Order | Fee (% of membership value) |
|-------------|----------------------------|
| 1st | Free (included) |
| 2nd | 10% + 150/year retroactive |
| 3rd | 20% + 200/year retroactive |
| 4th+ | 30% + 300/year retroactive |

| Child Age | Fee (% of membership value) |
|-----------|----------------------------|
| Under 18 (first 2-3) | Free (included) |
| Under 18 (4th+) | 5% |
| 18 | 10% |
| 19 | 15% |
| 20 | 20% |
| 21-24 | 15% (temporary status) |
| 25+ | Auto-separated |

| Other | Amount |
|-------|--------|
| Form fee | 505 EGP (500 + 5 stamp) |
| Addition form fee (post-activation) | 570 EGP + annual subscription |
| Temporary member | 10% of membership value |
| Foreign member | $10,000 USD |
| Waiver fee | 30% of membership value |

---

## 16. Financial Year Convention

Financial year runs **July to June**:
- If month >= 7: FY = `thisYear/(thisYear+1)` → e.g. "2025/2026"
- If month < 7: FY = `(lastYear)/thisYear` → e.g. "2024/2025"

Annual subscription rates (2025/2026): Member=492, Spouse=492, Child=222, Temp=222, Dev fee=35

---

## Appendix: Files to Check Before Modifying Members

If you're about to change the Members module, check these files for side effects:

1. `app/Modules/Payments/bootstrap.php` — Event listeners for member payments
2. `app/Modules/Cashier/bootstrap.php` — Payment request processing → member activation
3. `app/Modules/Payments/Services/PaymentLifecycleService.php` — Void/cancel logic
4. `app/Modules/Installments/Services/ChequeService.php` — Cheque submission → activation
5. `app/Modules/Children/Services/ChildFeeCalculator.php` — Uses MembershipRulesService
6. `app/Modules/Spouses/Services/SpouseFeeCalculator.php` — Uses MembershipRulesService
7. `app/Modules/Temporary/Services/TemporaryFeeCalculator.php` — Uses FormFeeService
8. `app/Modules/Death/Controllers/DeathController.php` — Death transfer flow
9. `app/Modules/Divorce/Controllers/DivorceController.php` — Divorce transfer flow
10. `app/Modules/Transfers/Services/TransferProcessor.php` — Membership transfer logic
