# Death Module — Architecture Map

> **Last updated:** 2026-08-06 (show view: deceased name, membership dates, subscription status; index: full status labels; createNewSubscriptions dedup)
> **Status:** Living document — incrementally updated as new information is discovered

---

## 1. Purpose & Responsibilities

The Death module manages **membership transfer upon death** of a member or their dependents. It handles:
- Recording death of a primary member, spouse, or child
- Transferring membership to the primary spouse (inherits SAME membership number)
- Creating separate new memberships for secondary spouses (get NEW numbers)
- Assigning children to the new primary member or secondary spouses
- Assigning temporary members to the new primary member or secondary spouses
- Fee collection (form fee + annual subscription for ALL family members)
- Board review & trustee fee calculation before payment
- Form fill step (wife fills full membership application form before transfer completes)
- Form number tracking (رقم الاستمارة) and deceased member name on new membership
- Death certificate & inheritance notice documentation
- Archive snapshot of deceased member
- Full Arabic audit trail logging of all operations during completion

It does **NOT** directly manage:
- Separation/transfer logic for non-death scenarios (Transfers module)
- Payment processing (delegates to Cashier/Payments)
- Archive snapshots (delegates to Archive module)
- Membership number generation (delegates to Members module)

---

## 2. Directory & File Structure

```
app/Modules/Death/
├── bootstrap.php                          # Minimal — no permissions (reuses Transfers)
├── Routes.php                             # Web routes (8 routes)
├── Controllers/
│   └── DeathController.php               # Main controller (index, CRUD, form fill, payment, completion)
├── Models/
│   └── DeathCase.php                     # Main model (no soft delete)
├── Services/
│   └── DeathAuditService.php             # Arabic audit log for death case operations
└── Views/
    ├── index.php                          # Paginated death case list
    ├── create.php                         # New death case form (select deceased type, spouse assignment)
    ├── form_fill.php                      # Full membership application for receiving spouse
    └── show.php                           # Case detail view with status/actions
```

---

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

### 3.1 `death_cases` Table (6 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 (the member associated with case) |
| deceased_type | varchar(30) | NO | | | primary_member / spouse / child |
| death_date | date | NO | MUL | | Date of death |
| death_certificate_number | varchar(100) | YES | | | Certificate reference |
| death_certificate_path | varchar(500) | YES | | | Uploaded certificate file path |
| spouse_id | bigint unsigned | YES | MUL | | FK to spouses (if deceased=spouse, or legacy) |
| primary_spouse_id | bigint unsigned | YES | | | FK to spouses (receives membership number) |
| secondary_spouses_json | text | YES | | | JSON array of spouse IDs getting separate memberships |
| primary_spouse_form_filled | tinyint(1) | NO | | 0 | Flag: wife has filled the membership form |
| children_assignment_json | json | YES | | | JSON map: child_id → 'primary' or spouse_id |
| child_id | bigint unsigned | YES | MUL | | FK to children (if deceased=child) |
| transferred_to_member_id | bigint unsigned | YES | MUL | | FK to members (new member created on completion) |
| same_membership_number | tinyint(1) | NO | | 0 | Flag: primary spouse inherits same number |
| fee_amount | decimal(15,2) | YES | | | Total fee for the case |
| archive_snapshot_id | bigint unsigned | YES | MUL | | FK to archive_snapshots |
| workflow_instance_id | bigint unsigned | YES | MUL | | FK to workflow_instances |
| status | varchar(50) | NO | MUL | recorded | recorded/fee_paid/pending_form_fill/completed |
| notes | text | YES | | | JSON (stores form_data for completion step) |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |
| updated_at | timestamp | NO | | auto-update | |
| created_by | bigint unsigned | YES | | | FK to employees |
| updated_by | bigint unsigned | YES | | | FK to employees |

**Current data distribution (status):**
- completed: 3
- fee_paid: 3

### 3.2 `death_case_audit_log` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| death_case_id | bigint unsigned | NO | MUL | | FK to death_cases |
| action_type | varchar(50) | NO | | | snapshot_taken, member_archived, new_member_created, etc. |
| action_description_ar | text | NO | | | Full Arabic description of the action |
| affected_entity_type | varchar(50) | YES | MUL | | Entity type affected (members, spouses, etc.) |
| affected_entity_id | bigint unsigned | YES | | | Entity ID affected |
| performed_by | bigint unsigned | YES | | | FK to employees |
| performed_at | timestamp | NO | | CURRENT_TIMESTAMP | |
| details_json | json | YES | | | Additional structured details |

**Action types:** snapshot_taken, member_archived, new_member_created, children_transferred, temps_transferred, secondary_member_created, installments_transferred, documents_linked, subscriptions_created, transfer_completed, case_completed, spouse_deceased, child_deceased

---

## 4. Death Case Status Lifecycle

```
recorded → board_review → board_approved → fee_paid → pending_form_fill → completed
                                                        (primary_member only)
                         → rejected (if board rejects)

recorded → completed (for spouse/child death — no fee, simpler flow)
```

**Status values:** recorded, board_review, board_approved, rejected, fee_paid, pending_form_fill, completed

**Key invariant:** For `primary_member` death, status must pass through `pending_form_fill` to ensure the receiving spouse fills out a full membership application before transfer completes.

---

## 5. Core Business Flows

### 5.1 Primary Member Death (Most Complex)

```
1. GET /death/create/{memberId}
   - Show spouses for primary/secondary selection
   - Show children for assignment
   - Display fee breakdown

2. POST /death/store/{memberId}
   - Validate: deceased_type='primary_member', death_date required
   - Require primary_spouse_id (who inherits membership number)
   - Optionally accept secondary_spouse_ids[] (get separate memberships)
   - Optionally accept children_assignment map
   - Fee = (form_fee + annual_sub) × (1 + secondary_count)
   - Create DeathCase (status='recorded')
   - Dispatch: death.recorded

3. POST /death/{id}/board-approve
   - Board reviews case, sets trustee fee (percentage of current plan price OR fixed)
   - Calculates total fee (base fees + trustee fee)
   - Creates payment request in Cashier queue
   - Status → 'board_approved'

4. Payment via Cashier queue
   - Process payment (type='death_fee')
   - Status → 'pending_form_fill' (NOT 'fee_paid' for primary)
   - Dispatch: death.fee_paid

5. GET /death/{id}/fill-form
   - Display full membership form pre-populated with spouse data

6. POST /death/{id}/fill-form
   - Validate required fields, store form_data in notes JSON
   - Set primary_spouse_form_filled = 1

7. POST /death/{id}/complete
   - Preconditions: board_approved + payment confirmed + form filled
   - Transaction:
     a. Archive snapshot of entire member + relations (ArchiveService::takeSnapshot)
     b. Archive deceased member (status='deceased', membership_number=NULL, is_archived=1)
        — All original data preserved, only status fields change
     c. Create new member with SAME membership_number
        — membership_value = CURRENT plan price (not old value)
        — join_date = today
     d. Archive primary spouse record (status='transferred')
     e. Assign children per children_assignment_json (or all to primary)
     f. For EACH secondary spouse: create new member (NEW number), move assigned children
     g. Transfer temporary_members to appropriate new member
     h. **Transfer active installment plans** to new member (schedule + cheques follow via FK)
     i. **Duplicate documents** to new member (originals stay with archived)
     j. **Create annual subscriptions** for new membership (current financial year)
     k. Record number chain via ArchiveService
     l. **Full Arabic audit log** via DeathAuditService (every step recorded)
     m. Status → 'completed'
   - Dispatch: death.completed
```

### 5.2 Spouse Death

```
1. Store: deceased_type='spouse', fee_amount=0
2. Complete: Archive spouse record (status='deceased', is_archived=1)
   No membership transfer needed.
```

### 5.3 Child Death

```
1. Store: deceased_type='child', fee_amount=0
2. Complete: Archive child record (status='deceased', is_archived=1)
   No membership transfer needed.
```

### 5.4 Fee Calculation

```
For primary_member death:
  The deceased member is NOT charged. The primary spouse IS the new member.

  Form Fee = SVC_TRANSFER_FORM (default 570 EGP)
  Annual Sub = 1 × SVC_ANNUAL_MEMBER (primary spouse = new member)
             + (remaining_spouses × SVC_ANNUAL_SPOUSE)     [exclude primary + secondaries]
             + (children × SVC_ANNUAL_CHILD)
             + (temps × SVC_ANNUAL_TEMP)
             + DEVELOPMENT_FEE (once)
  
  Example: new member (wife) + 0 extra spouses + 2 children + 1 temp = 492 + 222×2 + 222 + 35 = 1,193 EGP
  
  Base per membership = Form Fee + Annual Sub (new membership family)
  Total Base = Base × (1 + number_of_secondary_spouses)
  
  Trustee Fee = percentage of CURRENT plan price from pricing_configs (NOT old membership_value)
                OR fixed amount — set during board approval (boardApprove action)
  
  Grand Total = Total Base + Trustee Fee
  
  Payment receipt is created in the PRIMARY SPOUSE's name (not the deceased).

For spouse/child death: fee_amount = 0 (no transfer)
```

---

## 6. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `death.recorded` | `{case_id, member_id, type}` | DeathController::store |
| `death.fee_paid` | `{case_id, payment_id}` | DeathController::pay |
| `death.completed` | `{case_id, new_member_id}` | DeathController::complete |

---

## 7. Events Consumed (by other modules)

| Event | Listener Module | Action |
|-------|----------------|--------|
| `death.fee_paid` | Cashier | Updates `death_cases.status` to 'fee_paid' (via payment_request.completed handler) |

**Note:** Unlike Transfers and Waivers, there is NO Accounting listener for `death.completed`. The payment itself (death_fee) is captured by the generic `payment.completed` accounting handler.

---

## 8. Cross-Module Dependencies

### 8.1 Death module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Members | Member model, NationalIdParser (NID parsing in form fill), MemberNumberGenerator::assign (for secondary spouses) |
| Archive | ArchiveService (takeSnapshot, recordNumberTransfer) |
| Payments | PaymentService (processPayment for direct pay) |
| Cashier | PaymentRequestService (queue payment for cashier processing) |
| Pricing | PricingEngine::getMembershipPrice (current plan price for trustee fee calculation) |
| Rules | RuleEngine (DEVELOPMENT_FEE) |
| ServiceCatalog | ServicePrice (SVC_TRANSFER_FORM, SVC_ANNUAL_MEMBER, SVC_ANNUAL_SPOUSE, SVC_ANNUAL_CHILD, SVC_ANNUAL_TEMP) |

### 8.2 Other modules that IMPORT FROM Death:

| Module | What it imports |
|--------|----------------|
| Members | References death_cases table; `transferred_from_divorce_id` column exists on members |
| Cashier | Maps death_fee payment type → death_cases table |

---

## 9. Permissions

The Death module has **no own permissions**. It reuses Transfers module permissions:

| Key | Used For |
|-----|----------|
| transfer.view | View death case list and details |
| transfer.initiate | Create death cases, fill forms |
| transfer.approve | Complete death transfers |
| payment.collect | Process payment |

---

## 10. Configuration Dependencies

| Service Catalog Codes | Purpose |
|----------------------|---------|
| SVC_TRANSFER_FORM | Form fee (default 570 EGP) |
| SVC_ANNUAL_MEMBER | Annual subscription base — member (default 492 EGP) |
| SVC_ANNUAL_SPOUSE | Annual subscription — spouse (default 492 EGP) |
| SVC_ANNUAL_CHILD | Annual subscription — child (default 222 EGP) |
| SVC_ANNUAL_TEMP | Annual subscription — temp member (default 222 EGP) |

| Rule Engine Keys | Purpose |
|-----------------|---------|
| DEVELOPMENT_FEE | Added to annual subscription (default 35 EGP) |

---

## 11. High-Risk Areas

### 11.1 Primary Member Death Completion (CRITICAL)
- Most complex transaction in the ERP: archives member, creates 1+ new members, moves children, archives spouses
- Source member's `membership_number` set to NULL first, then primary spouse gets it (UNIQUE constraint handling)
- If secondary spouses exist: each gets a NEW number via `MemberNumberGenerator::assign()`
- Children assignment is flexible: can go to primary, to secondary spouses, or default all to primary
- Transaction rollback protects against partial completion

### 11.2 Form Data in JSON Notes
- The wife's completed membership form is stored as JSON in `death_cases.notes`
- This data is read back during `complete()` to populate the new member record
- If `notes` JSON is corrupted or missing keys: new member gets sparse data
- No validation that stored form_data still matches required fields at completion time

### 11.3 Membership Number Inheritance
- Primary spouse inherits the EXACT same membership number as the deceased member
- Number is released (set NULL on source) then assigned to new record in same transaction
- Race condition window is minimal (within transaction) but exists if DB lock is lost

### 11.4 Secondary Spouse Number Assignment
- Secondary spouses get NEW sequential numbers via `MemberNumberGenerator::assign()`
- This method queries MAX(membership_number) — potential race condition under concurrency
- No `activated_by_payment_id` fallback if death_fee payment cannot be found

### 11.5 Fee Paid Status Split
- For `primary_member` death: after payment, status goes to `pending_form_fill` (not `fee_paid`)
- For `spouse`/`child` death: no payment needed, goes directly to completion
- The Cashier listener ALWAYS sets status to `fee_paid` — so for primary_member death, there's a conflict:
  - Controller sets `pending_form_fill` directly
  - If Cashier processes it, it would set `fee_paid` instead
  - The controller `pay()` handles this by checking `deceased_type` and choosing status accordingly

---

## 12. Known Patterns & Gotchas

1. **No bootstrap permissions**: Death module has an empty bootstrap — permissions come from Transfers module
2. **Menu in Members module**: Death menu item registered in `Members/bootstrap.php`
3. **No soft delete on model**: DeathCase records are permanent (no is_archived column)
4. **Form fill is mandatory for primary death**: Cannot complete without `primary_spouse_form_filled = 1`
5. **Children default to primary**: If `children_assignment_json` is empty, ALL children go to new primary member
6. **Temporary members can be assigned**: Distribution UI (like children) allows assigning temps to primary or secondary spouses
7. **activated_by_payment_id**: New member records link to the death_fee payment ID for audit trail
8. **Spouse/child death is simpler**: Just archives the dependent record, no membership transfer
9. **Dual payment paths**: Payment via Cashier queue (normal) or direct via `/pay` endpoint
10. **show() enhanced data (2026-08-06)**: Controller passes `deceasedName` (actual spouse/child name), `newMemberSubs` (subscription rows for transferred member), `membership_created_at`/`membership_activated_at` from the members join, and `created_at`/`activated_at` from newMember query. show.php displays deceased person name, case creation date, original membership dates, new member acquisition date, and subscription status section.
11. **index.php full status labels (2026-08-06)**: All death case statuses (recorded, board_review, board_approved, rejected, fee_paid, pending_form_fill, completed) now have proper Arabic status labels displayed in the list view.
12. **createNewSubscriptions dedup (2026-08-06)**: The method checks for existing subscription rows before inserting and updates pending rows to paid status instead of creating duplicates — prevents double subscription entries on re-completion or retry scenarios.

---

## Appendix: Files to Check Before Modifying Death

1. `app/Modules/Archive/Services/ArchiveService.php` — snapshot and number chain logic
2. `app/Modules/Cashier/bootstrap.php` — payment_request.completed listener (death_fee → fee_paid)
3. `app/Modules/Members/Services/MemberNumberGenerator.php` — assign() for secondary spouses
4. `app/Modules/Members/bootstrap.php` — menu registration
5. `app/Modules/ServiceCatalog/Models/ServicePrice.php` — SVC_TRANSFER_FORM, SVC_ANNUAL_MEMBER lookups
6. `app/Modules/Rules/Services/RuleEngine.php` — DEVELOPMENT_FEE rule
