Commit 552ca604 authored by Mahmoud Aglan's avatar Mahmoud Aglan

docs: update Waiver architecture map with bylaws compliance flow

Reflects new debt verification, excess fee calculation, document uploads,
and two-phase dependent validation added in the rewrite.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 8b8f9ee3
# Waiver Module — Architecture Map # Waiver Module — Architecture Map
> **Last updated:** 2026-06-19 (fixed duplicate key error + fee uses current pricing) > **Last updated:** 2026-06-22 (comprehensive bylaws compliance rewrite)
> **Status:** Living document — incrementally updated as new information is discovered > **Status:** Living document — incrementally updated as new information is discovered
--- ---
...@@ -45,7 +45,7 @@ app/Modules/Waiver/ ...@@ -45,7 +45,7 @@ app/Modules/Waiver/
## 3. Database Schema (Production — Source of Truth) ## 3. Database Schema (Production — Source of Truth)
### 3.1 `waiver_requests` Table (9 rows as of 2026-06-10) ### 3.1 `waiver_requests` Table (18 rows as of 2026-06-22)
| Column | Type | Nullable | Key | Default | Notes | | Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------| |--------|------|----------|-----|---------|-------|
...@@ -57,12 +57,18 @@ app/Modules/Waiver/ ...@@ -57,12 +57,18 @@ app/Modules/Waiver/
| waiver_fee_percentage | decimal(5,2) | NO | | 30.00 | Fee percentage (from WAIVER_FEE rule) | | waiver_fee_percentage | decimal(5,2) | NO | | 30.00 | Fee percentage (from WAIVER_FEE rule) |
| waiver_fee_amount | decimal(15,2) | NO | | | Calculated: percentage * membership_value | | waiver_fee_amount | decimal(15,2) | NO | | | Calculated: percentage * membership_value |
| original_dependent_count | int unsigned | NO | | 0 | Total dependents at time of waiver | | original_dependent_count | int unsigned | NO | | 0 | Total dependents at time of waiver |
| new_dependent_count | int unsigned | YES | | | Target member's dependent count (future) | | new_dependent_count | int unsigned | YES | | | Target member's dependent count (at completion) |
| excess_dependent_count | int unsigned | NO | | 0 | How many dependents exceed the original count |
| excess_fee_percentage | decimal(5,2) | YES | | NULL | Board-determined % for excess dependents |
| excess_fee_amount | decimal(15,2) | NO | | 0.00 | = excess_fee_pct × membership_value × excess_count |
| board_approval_required | tinyint(1) | NO | | 1 | Always requires board approval | | board_approval_required | tinyint(1) | NO | | 1 | Always requires board approval |
| board_decision_reference | varchar(100) | YES | | | Board decision document reference | | board_decision_reference | varchar(100) | YES | | | Board decision document reference |
| approved_by | bigint unsigned | YES | | | FK to employees | | approved_by | bigint unsigned | YES | | | FK to employees |
| approved_at | timestamp | YES | | | | | approved_at | timestamp | YES | | | |
| annual_renewal_paid | tinyint(1) | NO | | 0 | Flag: annual renewal has been paid | | annual_renewal_paid | tinyint(1) | NO | | 0 | Flag: annual renewal has been paid |
| debts_cleared | tinyint(1) | NO | | 0 | Flag: all financial debts verified clear |
| waiver_request_doc_path | varchar(500) | YES | | NULL | Uploaded waiver request form document |
| target_form_doc_path | varchar(500) | YES | | NULL | Uploaded target member form document |
| archive_snapshot_id | bigint unsigned | YES | MUL | | FK to archive_snapshots | | archive_snapshot_id | bigint unsigned | YES | MUL | | FK to archive_snapshots |
| workflow_instance_id | bigint unsigned | YES | MUL | | FK to workflow_instances | | workflow_instance_id | bigint unsigned | YES | MUL | | FK to workflow_instances |
| status | varchar(50) | NO | MUL | requested | requested/approved/fee_paid/completed/rejected | | status | varchar(50) | NO | MUL | requested | requested/approved/fee_paid/completed/rejected |
...@@ -73,7 +79,7 @@ app/Modules/Waiver/ ...@@ -73,7 +79,7 @@ app/Modules/Waiver/
| updated_by | bigint unsigned | YES | | | FK to employees | | updated_by | bigint unsigned | YES | | | FK to employees |
**Current data distribution (status):** **Current data distribution (status):**
- fee_paid: 9 - completed: 2, requested: 7, approved: 1, fee_paid: 8
--- ---
...@@ -93,37 +99,45 @@ requested → approved → fee_paid → completed ...@@ -93,37 +99,45 @@ requested → approved → fee_paid → completed
## 5. Core Business Flows ## 5. Core Business Flows
### 5.1 Waiver Request Flow ### 5.1 Waiver Request Flow (Bylaws-Compliant — 2026-06-22)
``` ```
1. GET /waivers/create/{memberId} 1. GET /waivers/create/{memberId}
- Validate member exists and is not archived - Validate member exists and is not archived
- Run WaiverProcessor::checkDebts(memberId) — checks subscriptions, fines, payment_requests
- If debts exist: show debt table, BLOCK form submission
- Calculate waiver fee: WAIVER_FEE percentage × CURRENT membership value (from pricing_configs) - Calculate waiver fee: WAIVER_FEE percentage × CURRENT membership value (from pricing_configs)
- Count dependents (spouses + children + temporary) - Run WaiverProcessor::countDependents(memberId) — counts spouses, children, temporary
- Display fee and dependent summary - Display: fee breakdown, dependent grid, debt status, document upload fields
2. POST /waivers/store/{memberId} 2. POST /waivers/store/{memberId}
- Validate: member exists, has a membership_number - VALIDATE: member exists, has membership_number
- Calculate fee: WAIVER_FEE (default 30%) × current membership value (from pricing_configs, NOT stored membership_value) - VALIDATE: checkDebts → must be clear (blocks if any unpaid)
- Calculate fee: WAIVER_FEE (default 30%) × current pricing_configs value
- Count all active dependents (spouses + children + temp) - Count all active dependents (spouses + children + temp)
- Handle file uploads: waiver_request_doc, target_form_doc (PDF/JPG/PNG, max 5MB)
- Optionally accept target_member_id - Optionally accept target_member_id
- Create WaiverRequest (status='requested') - Create WaiverRequest (status='requested', debts_cleared=1)
- Dispatch: waiver.requested - Dispatch: waiver.requested
- Send payment request to Cashier queue (waiver_fee amount) - Send payment request to Cashier queue (waiver_fee amount)
3. POST /waivers/{id}/approve 3. POST /waivers/{id}/approve
- Must be status='requested' - Must be status='requested'
- Set approved_by, approved_at, board_decision_reference - Board sets: board_decision_reference, excess_fee_percentage (if target has excess dependents)
- If target exists AND target dependents > original count:
excess_count = target_deps - original_count
excess_fee = (excess_fee_percentage × membership_value / 100) × excess_count
- Set approved_by, approved_at, excess_dependent_count, excess_fee_amount
- Status → 'approved' - Status → 'approved'
- (No event dispatched for approve) - Sends combined payment request (waiver_fee + excess_fee) to Cashier
4. POST /waivers/{id}/reject 4. POST /waivers/{id}/reject
- Must be status='requested' - Must be status='requested'
- Accepts rejection_reason in notes
- Status → 'rejected' - Status → 'rejected'
- (No event dispatched for reject)
5. POST /waivers/{id}/pay (or via Cashier queue) 5. POST /waivers/{id}/pay (or via Cashier queue)
- Process payment (type='waiver_fee') - Process payment: waiver_fee + excess_fee combined
- Status → 'fee_paid' - Status → 'fee_paid'
- Dispatch: waiver.fee_paid - Dispatch: waiver.fee_paid
...@@ -131,18 +145,19 @@ requested → approved → fee_paid → completed ...@@ -131,18 +145,19 @@ requested → approved → fee_paid → completed
- Optionally update target_member_id from POST data - Optionally update target_member_id from POST data
- PRE-VALIDATION (before processor runs): - PRE-VALIDATION (before processor runs):
a. Target member must be set a. Target member must be set
b. All annual subscriptions for source member must be paid (no pending/overdue) b. checkDebts(source) — ALL financial obligations must be clear
c. Target member's dependent count must NOT exceed source's original_dependent_count c. countDependents(target) check:
- If exceeded: blocks with error, requires board approval + extra fees first - If target > original AND no excess_fee_amount set → block (board must approve first)
d. Records new_dependent_count and marks annual_renewal_paid = 1 d. Status must be 'approved' or 'fee_paid'
e. Records new_dependent_count, debts_cleared=1
- Execute WaiverProcessor::execute(): - Execute WaiverProcessor::execute():
a. Validate: status must be 'approved' or 'fee_paid' a. Validate: status must be 'approved' or 'fee_paid'
b. Validate: target_member_id must be set b. Validate: target_member_id must be set
c. Transaction: c. Transaction:
- Archive snapshot of source member (full data preserved in archive_snapshots) - Archive snapshot of source member (full data preserved in archive_snapshots)
- Archive source member FIRST (number=NULL, status='waived', is_archived=1) — releases unique constraint - Archive source member FIRST (number=NULL, status='waived', is_archived=1) — releases unique constraint
- Transfer membership_number to target member (set number, status='active') - Transfer membership_number to target member (set number, status='active', membership_type inherited)
- Record number chain via ArchiveService - Record number chain via ArchiveService::recordNumberTransfer
- Status → 'completed' - Status → 'completed'
d. Dispatch: waiver.completed d. Dispatch: waiver.completed
- POST-STATE: - POST-STATE:
...@@ -154,23 +169,43 @@ requested → approved → fee_paid → completed ...@@ -154,23 +169,43 @@ requested → approved → fee_paid → completed
### 5.2 Fee Calculation ### 5.2 Fee Calculation
``` ```
Waiver Fee = WAIVER_FEE percentage × CURRENT membership value (from pricing_configs) BASIC Waiver Fee = WAIVER_FEE percentage × CURRENT membership value (from pricing_configs)
Default: 30% × current_price_for(branch_id, qualification_id, membership_type) Default: 30% × current_price_for(branch_id, qualification_id, membership_type)
Example: 30% × 150,000 = 45,000 EGP Example: 30% × 150,000 = 45,000 EGP
EXCESS Dependent Fee (if target has more dependents than source):
excess_count = target_dependents - original_dependent_count
excess_fee = (board_percentage / 100) × membership_value × excess_count
Example: 2 excess × 30% × 150,000 = 90,000 EGP
TOTAL = waiver_fee + excess_fee
Example: 45,000 + 90,000 = 135,000 EGP
Falls back to stored membership_value only if no matching pricing_configs row exists. Falls back to stored membership_value only if no matching pricing_configs row exists.
No additional form fee or annual subscription (unlike Transfers/Death/Divorce). No additional form fee or annual subscription (unlike Transfers/Death/Divorce).
``` ```
### 5.3 Eligibility (from TransferEligibility::canWaiver) ### 5.3 Eligibility & Validation (Bylaws)
``` ```
Requirements: PRE-REQUISITES (enforced in create + store):
1. Member must exist and not be archived 1. Member must exist and not be archived
2. Member status must be 'active' 2. Member must have a membership_number
3. No unpaid subscriptions (pending/overdue) 3. ALL financial debts must be clear:
- subscriptions (status: pending/overdue) = 0
- fines (status: pending) = 0
- payment_requests (status: pending, not voided) = 0
4. Board approval always required 4. Board approval always required
DEPENDENT RULES (enforced in approve + complete):
- Scenario 1: target dependents ≤ original → OK, no extra fees
- Scenario 2: target dependents > original → board sets excess_fee_percentage
- fee = percentage × membership_value × excess_count per dependent
REQUIRED DOCUMENTS:
- waiver_request_doc: waiver request form (PDF/JPG/PNG)
- target_form_doc: target member's membership form (PDF/JPG/PNG)
``` ```
--- ---
...@@ -271,11 +306,11 @@ The Waiver module uses permissions registered in **Transfers/bootstrap.php**: ...@@ -271,11 +306,11 @@ The Waiver module uses permissions registered in **Transfers/bootstrap.php**:
- WaiverProcessor accepts BOTH 'approved' and 'fee_paid' for completion - WaiverProcessor accepts BOTH 'approved' and 'fee_paid' for completion
- This means: payment can be collected even if board eventually rejects (refund needed) - This means: payment can be collected even if board eventually rejects (refund needed)
### 11.6 No Unpaid Subscription Check at Completion ### 11.6 ~~No Unpaid Subscription Check at Completion~~ FIXED (2026-06-22)
- Eligibility check (TransferEligibility::canWaiver) verifies no unpaid subscriptions - **NOW ENFORCED**: WaiverProcessor::checkDebts() is called at create, store, AND complete
- BUT this check is in the Transfers module, not called by WaiverController - Checks ALL financial obligations: subscriptions, fines, AND payment_requests
- WaiverController::store() does NOT call TransferEligibility::canWaiver() - Form is blocked (disabled) if any debts exist — cannot even submit the request
- Gap: member could have unpaid subscriptions and still submit a waiver - Complete also re-validates debts in case new ones arose between request and completion
--- ---
...@@ -289,7 +324,10 @@ The Waiver module uses permissions registered in **Transfers/bootstrap.php**: ...@@ -289,7 +324,10 @@ The Waiver module uses permissions registered in **Transfers/bootstrap.php**:
6. **30% default fee**: Configurable via WAIVER_FEE rule 6. **30% default fee**: Configurable via WAIVER_FEE rule
7. **No form fee or annual sub**: Simpler fee structure than Transfers/Death/Divorce 7. **No form fee or annual sub**: Simpler fee structure than Transfers/Death/Divorce
8. **Payment sent immediately**: Cashier queue payment is created at request time (before approval) 8. **Payment sent immediately**: Cashier queue payment is created at request time (before approval)
9. **9 fee_paid records exist**: All 9 existing records are in fee_paid status (none completed yet) 9. **Debt gate**: Form is BLOCKED if any debts exist — verified at create, store, and complete
10. **Excess fee board-determined**: Board enters % during approval; formula is pct × value × excess_count
11. **Document uploads**: Stored in `uploads/waivers/YYYY/MM/` — PDF, JPG, PNG up to 5MB
12. **Two-phase dependent validation**: at approval (board sets fee) + at completion (final check)
10. **board_approval_required always 1**: Column exists but is hardcoded to always require board 10. **board_approval_required always 1**: Column exists but is hardcoded to always require board
11. **No soft delete**: WaiverRequest has no is_archived column 11. **No soft delete**: WaiverRequest has no is_archived column
12. **Dual payment paths**: Cashier queue on creation + direct /pay endpoint 12. **Dual payment paths**: Cashier queue on creation + direct /pay endpoint
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment