# Divorce Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Divorce module manages **membership creation for a divorced spouse** — the process of granting an independent working membership to a spouse after divorce. It handles:
- Recording divorce cases with certificate upload
- Eligibility validation (1-year window from divorce date, 5-year membership minimum)
- Board of Trustees review workflow (board sets the fee percentage)
- Fee calculation (board-determined percentage of membership value + form fee + annual subscription)
- Creating a new member record for the divorced spouse (gets a NEW membership number)
- Preserving the original member's membership (unlike death/waiver, original stays active)

It does **NOT** directly manage:
- Original member's status change (original member stays active and unaffected)
- Payment processing (delegates to Cashier/Payments)
- Number generation (delegates to Members module)
- Archive snapshots (delegates to Archive module)

---

## 2. Directory & File Structure

```
app/Modules/Divorce/
├── bootstrap.php                          # Minimal — no permissions (reuses Transfers)
├── Routes.php                             # Web routes (8 routes)
├── Controllers/
│   └── DivorceController.php             # Main controller (index, CRUD, board decision, payment, completion)
├── Models/
│   └── DivorceCase.php                   # Main model (no soft delete)
├── Services/
│   └── DivorceFeeCalculator.php          # Eligibility validation, form fee, annual sub calculation
└── Views/
    ├── index.php                          # Paginated divorce case list
    ├── create.php                         # New divorce case form (select spouse, upload certificate)
    └── show.php                           # Case detail view with board decision, payment, completion
```

---

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

### 3.1 `divorce_cases` Table (3 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 original member) |
| spouse_id | bigint unsigned | NO | MUL | | FK to spouses (the divorced spouse) |
| divorce_date | date | NO | MUL | | Date of divorce (from certificate) |
| divorce_certificate_path | varchar(500) | YES | | | Uploaded certificate file |
| spouse_qualification_id | bigint unsigned | YES | | | Qualification for new member (board decides) |
| divorce_case_type | varchar(50) | NO | | | same_form / joined_after |
| request_date | date | NO | | | Date case was submitted |
| membership_acquisition_date | date | YES | | | When original member joined |
| years_of_membership | int unsigned | YES | | | Years since member joined |
| has_children_on_membership | tinyint(1) | NO | | 0 | Flag: children exist (waives 5-year rule) |
| fee_percentage | decimal(5,2) | YES | | | Board-determined percentage |
| fee_amount | decimal(15,2) | YES | | | percentage * membership_value |
| annual_subscription_fee | decimal(15,2) | YES | | 0.00 | Annual sub for new member |
| form_fee | decimal(15,2) | YES | | 0.00 | Form fee (570 EGP) |
| total_fee | decimal(15,2) | YES | | 0.00 | fee_amount + form_fee + annual_sub |
| board_decision_number | varchar(50) | YES | | | Board decision reference number |
| board_decision_date | date | YES | | | Date of board decision |
| board_notes | text | YES | | | Board notes/reasoning |
| spouse_new_member_id | bigint unsigned | YES | MUL | | FK to members (new member created) |
| spouse_new_membership_number | varchar(20) | YES | | | The new membership number assigned |
| original_member_id | bigint unsigned | YES | | | FK to members (redundant with member_id) |
| original_membership_number | varchar(20) | YES | | | Original member's number (preserved) |
| children_assignment_json | json | YES | | | Future: assign children between members |
| 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 | submitted | board_review/board_approved/fee_paid/completed/rejected |
| notes | text | YES | | | Free text notes |
| 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):**
- board_review: 1
- completed: 2

---

## 4. Divorce Case Status Lifecycle

```
board_review → board_approved → fee_paid → completed
      ↓
   rejected
```

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

**Key invariant:** The board of trustees must FIRST approve and set the fee percentage before payment can be collected. This is unlike transfers where fees are calculated automatically.

---

## 5. Core Business Flows

### 5.1 Divorce Transfer Flow

```
1. GET /divorce/create/{memberId}
   - Validate member has active spouses
   - Show spouses for selection
   - Show qualifications for new member's qualification

2. POST /divorce/store/{memberId}
   - Validate: spouse_id, divorce_date, spouse_qualification_id required
   - Upload divorce certificate (REQUIRED — PDF/JPEG/PNG, max 5MB)
   - Validate eligibility via DivorceFeeCalculator::calculate():
     a. Check 1-year window from divorce_date (DIVORCE_REQUEST_WINDOW rule)
     b. Check 5-year minimum membership (DIVORCE_MIN_MEMBERSHIP_YEARS rule)
     c. Waived if children exist (waived_if_children flag)
   - Determine case_type: 'same_form' (joined same day) or 'joined_after'
   - Create DivorceCase (status='board_review')
   - Dispatch: divorce.submitted

3. POST /divorce/{id}/board-approve
   - Board enters: fee_percentage, board_decision_number, board_decision_date, notes
   - Calculate fees:
     Fee = (fee_percentage / 100) × membership_value
     Total = Fee + form_fee (570) + annual_subscription (222 + 35 = 257)
   - Status → 'board_approved'
   - Auto-send payment request to Cashier queue
   - Dispatch: divorce.board_approved

4. POST /divorce/{id}/board-reject
   - Status → 'rejected'
   - Store board_notes
   - Dispatch: divorce.rejected

5. POST /divorce/{id}/pay  (or via Cashier queue)
   - Process payment (type='divorce_fee')
   - Status → 'fee_paid'
   - Dispatch: divorce.fee_paid

6. POST /divorce/{id}/complete
   - Must be 'board_approved' or 'fee_paid'
   - Transaction:
     a. Archive snapshot of original member
     b. Create NEW member from spouse data:
        - full_name, national_id, DOB, gender from spouse record
        - qualification from spouse_qualification_id
        - branch from original member
        - membership_value from original member
        - transferred_from_divorce_id = case ID
        - original_membership_number = member's number
     c. Assign NEW membership number via MemberNumberGenerator::assign()
     d. Archive spouse record (status='divorced', is_archived=1)
     e. Update case: spouse_new_member_id, spouse_new_membership_number, original refs
     f. Record number chain via ArchiveService
     g. Status → 'completed'
   - Dispatch: divorce.completed
```

### 5.2 Fee Calculation Logic (DivorceFeeCalculator)

```
Eligibility Check:
  1. Divorce date must be within DIVORCE_REQUEST_WINDOW (default 1 year)
  2. Member must have >= DIVORCE_MIN_MEMBERSHIP_YEARS (default 5 years)
     EXCEPTION: Waived if member has children (waived_if_children = true)

Case Type:
  - 'same_form': spouse joined on SAME DATE as member (joined together)
  - 'joined_after': spouse joined AFTER member acquired membership

Fee Components:
  - Form Fee: FORM_TRANSFER_FEE rule OR SVC_ADDITION_FORM (default 570 EGP)
  - Annual Subscription: SVC_ANNUAL_CHILD rate + DEVELOPMENT_FEE (default 222 + 35 = 257 EGP)
  - Percentage Fee: SET BY BOARD (not calculated automatically)
    Typical: same_form = 10%, joined_after = 50% (from TransferEligibility reference)
    BUT board can set ANY percentage

Total = (percentage × membership_value) + form_fee + annual_subscription
```

---

## 6. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `divorce.submitted` | `{case_id, member_id}` | DivorceController::store |
| `divorce.board_approved` | `{case_id, fee_percentage}` | DivorceController::boardApprove |
| `divorce.rejected` | `{case_id}` | DivorceController::boardReject |
| `divorce.fee_paid` | `{case_id, payment_id}` | DivorceController::pay |
| `divorce.completed` | `{case_id, new_member_id, new_number}` | DivorceController::complete |

---

## 7. Events Consumed (by other modules)

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

**Note:** No Accounting listener for divorce events specifically. The divorce_fee payment is captured by the generic `payment.completed` accounting handler.

---

## 8. Cross-Module Dependencies

### 8.1 Divorce module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Members | MemberNumberGenerator::assign (new number for divorced spouse) |
| Archive | ArchiveService (takeSnapshot, recordNumberTransfer) |
| Payments | PaymentService (processPayment for direct pay) |
| Cashier | PaymentRequestService (queue payment after board approval) |
| Rules | RuleEngine (FORM_TRANSFER_FEE, DEVELOPMENT_FEE, DIVORCE_REQUEST_WINDOW, DIVORCE_MIN_MEMBERSHIP_YEARS) |
| ServiceCatalog | ServicePrice (SVC_ADDITION_FORM, SVC_ANNUAL_CHILD) |

### 8.2 Other modules that IMPORT FROM Divorce:

| Module | What it imports |
|--------|----------------|
| Members | `transferred_from_divorce_id` column on members table links back to divorce_cases |
| Cashier | Maps divorce_fee payment type → divorce_cases table |
| Transfers | TransferEligibility::canSpouseSeparateDivorce() references divorce rules |

---

## 9. Permissions

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

| Key | Used For |
|-----|----------|
| transfer.view | View divorce case list and details |
| transfer.initiate | Create divorce cases |
| transfer.approve | Board approve/reject, complete transfers |
| payment.collect | Process payment |

---

## 10. Configuration Dependencies

| Rule Engine Keys | Purpose |
|-----------------|---------|
| FORM_TRANSFER_FEE | Form fee amount (fallback for DivorceFeeCalculator::getFormFee) |
| DEVELOPMENT_FEE | Dev fee added to annual subscription |
| DIVORCE_REQUEST_WINDOW | Max years after divorce date to submit request (default 1) |
| DIVORCE_MIN_MEMBERSHIP_YEARS | Minimum years of membership required (default 5) |

| Service Catalog Codes | Purpose |
|----------------------|---------|
| SVC_ADDITION_FORM | Fallback form fee (570 EGP) |
| SVC_ANNUAL_CHILD% | Annual subscription rate (222 EGP) |

---

## 11. High-Risk Areas

### 11.1 Board-Dependent Fee Percentage (CRITICAL)
- Unlike Transfers (auto-calculated) and Death (fixed), divorce fees require BOARD to set percentage
- Board can set ANY percentage — no validation range (could be 0% or 100%)
- If board approves but staff completes without payment: status check prevents this (must be 'board_approved' or 'fee_paid')
- No way to edit the percentage after board_approved without direct DB access

### 11.2 Certificate Upload
- Divorce certificate is REQUIRED — cannot proceed without it
- Stored in `storage/uploads/divorce/` with timestamped filename
- No virus scanning or content validation beyond MIME type check
- If file is deleted from disk: path is still in DB, will cause broken reference

### 11.3 New Member from Spouse Data
- New member inherits data from the `spouses` table record
- Spouse data may be incomplete (missing address, employment info)
- No full membership form fill step (unlike Death module) — spouse gets minimal profile
- `qualification_id` comes from the divorce case form, not the spouse record

### 11.4 Original Member Stays Active
- UNIQUE among transfer-type modules: the original member is NOT archived
- Original member keeps their membership number and active status
- Only the spouse record is archived (status='divorced')
- If completion fails after spouse is archived but before new member is created: spouse is lost

### 11.5 Annual Subscription Uses CHILD Rate
- `DivorceFeeCalculator::getAnnualSubscription()` uses `SVC_ANNUAL_CHILD%` rate, not member rate
- This may be intentional (divorced spouse starts as "acquired" member with lower sub rate)
- But could be a bug if the intent was to charge full member subscription rate

### 11.6 Case Type Determination
- `same_form`: member creation date === spouse join date (joined together)
- `joined_after`: any other case
- The case type is informational for the board — board still manually sets percentage
- No `both_working` case type handled in the controller (only in TransferEligibility)

---

## 12. Known Patterns & Gotchas

1. **No bootstrap permissions**: Empty bootstrap — permissions come from Transfers module
2. **Menu in Members module**: Divorce menu item registered in `Members/bootstrap.php`
3. **Board sets the fee**: Unlike auto-calculated transfers, board manually enters percentage
4. **Original member unaffected**: Only module where source member keeps active status
5. **NEW number assigned**: Divorced spouse gets a new number (unlike death where primary inherits)
6. **Certificate required**: Cannot submit without uploading divorce certificate
7. **No form fill step**: Unlike Death module, no full membership form for the divorced spouse
8. **transferred_from_divorce_id**: The new member record has this FK back to the divorce case
9. **original_membership_number**: Preserved on new member for audit trail
10. **children_assignment_json**: Column exists but NOT implemented in completion logic (future)
11. **Dual payment paths**: Board approval auto-sends to Cashier queue; also has direct `/pay` endpoint

---

## 13. Case Type Labels (Arabic)

| Type Code | Arabic Label |
|-----------|-------------|
| both_working | كلاهما عامل قبل الزواج |
| same_form | قُبلا في استمارة واحدة |
| joined_after | الزوج/ة انضم بعد العضوية |

---

## Appendix: Files to Check Before Modifying Divorce

1. `app/Modules/Archive/Services/ArchiveService.php` — snapshot and number chain logic
2. `app/Modules/Cashier/bootstrap.php` — payment_request.completed listener (divorce_fee → fee_paid)
3. `app/Modules/Members/Services/MemberNumberGenerator.php` — assign() for new member
4. `app/Modules/Members/bootstrap.php` — menu registration
5. `app/Modules/Transfers/Services/TransferEligibility.php` — shared eligibility logic (canSpouseSeparateDivorce, getDivorceTransferFee)
6. `app/Modules/Rules/Services/RuleEngine.php` — DIVORCE_* rules
7. `app/Modules/ServiceCatalog/Models/ServicePrice.php` — SVC_ADDITION_FORM, SVC_ANNUAL_CHILD lookups
