Commit 8b8f9ee3 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(waiver): comprehensive bylaws compliance rewrite

Full implementation of waiver module per club bylaws:
- Debt verification (subscriptions + fines + payment requests) blocks form
- Dependent count comparison with excess detection
- Board sets excess fee percentage during approval
- Document upload support (waiver form + target membership form)
- Auto-computed fields (no manual input for existing members)
- Enhanced show view with dependent comparison and fee breakdown
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 63605b9a
......@@ -27,9 +27,11 @@ final class WaiverProcessor
$sourceMember = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $waiver['source_member_id']]);
if (!$sourceMember) return ['success' => false, 'error' => 'العضو المصدر غير موجود'];
$targetMember = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $waiver['target_member_id']]);
if (!$targetMember) return ['success' => false, 'error' => 'العضو المستفيد غير موجود'];
$db->beginTransaction();
try {
// Archive snapshot
$snapshotId = ArchiveService::takeSnapshot('members', (int) $sourceMember['id'], 'waiver', 'تنازل — طلب #' . $waiverId);
// Archive source member FIRST (release number to avoid unique constraint violation)
......@@ -42,17 +44,16 @@ final class WaiverProcessor
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $waiver['source_member_id']]);
// Transfer membership number to target member
// Transfer membership number to target — becomes the new primary member
$db->update('members', [
'membership_number' => $waiver['membership_number'],
'status' => 'active',
'membership_type' => $sourceMember['membership_type'] ?? 'working',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $waiver['target_member_id']]);
// Record number chain
ArchiveService::recordNumberTransfer($waiver['membership_number'], 'waiver', 'members', (int) $waiver['target_member_id']);
// Complete waiver
$db->update('waiver_requests', [
'archive_snapshot_id' => $snapshotId,
'status' => 'completed',
......@@ -77,4 +78,85 @@ final class WaiverProcessor
return ['success' => false, 'error' => $e->getMessage()];
}
}
}
\ No newline at end of file
/**
* Check all financial obligations for a member.
* Returns ['clear' => bool, 'debts' => [...details...]]
*/
public static function checkDebts(int $memberId): array
{
$db = App::getInstance()->db();
$debts = [];
// Unpaid subscriptions
$unpaidSubs = $db->selectOne(
"SELECT COUNT(*) as cnt, COALESCE(SUM(total_amount - paid_amount + fine_amount), 0) as total
FROM subscriptions WHERE member_id = ? AND status IN ('pending','overdue')",
[$memberId]
);
if ((int) ($unpaidSubs['cnt'] ?? 0) > 0) {
$debts[] = [
'type' => 'اشتراكات سنوية',
'count' => (int) $unpaidSubs['cnt'],
'amount' => $unpaidSubs['total'] ?? '0.00',
];
}
// Unpaid fines
$unpaidFines = $db->selectOne(
"SELECT COUNT(*) as cnt, COALESCE(SUM(amount - paid_amount), 0) as total
FROM fines WHERE member_id = ? AND status = 'pending'",
[$memberId]
);
if ((int) ($unpaidFines['cnt'] ?? 0) > 0) {
$debts[] = [
'type' => 'غرامات',
'count' => (int) $unpaidFines['cnt'],
'amount' => $unpaidFines['total'] ?? '0.00',
];
}
// Unpaid payment requests (cashier queue)
$unpaidRequests = $db->selectOne(
"SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total
FROM payment_requests WHERE member_id = ? AND status = 'pending' AND is_voided = 0",
[$memberId]
);
if ((int) ($unpaidRequests['cnt'] ?? 0) > 0) {
$debts[] = [
'type' => 'طلبات دفع معلقة',
'count' => (int) $unpaidRequests['cnt'],
'amount' => $unpaidRequests['total'] ?? '0.00',
];
}
$totalDebt = '0.00';
foreach ($debts as $d) {
$totalDebt = bcadd($totalDebt, (string) $d['amount'], 2);
}
return [
'clear' => empty($debts),
'debts' => $debts,
'total' => $totalDebt,
];
}
/**
* Count dependents for a member (spouses + children + temporary).
*/
public static function countDependents(int $memberId): array
{
$db = App::getInstance()->db();
$spouses = (int) ($db->selectOne("SELECT COUNT(*) as cnt FROM spouses WHERE member_id = ? AND is_archived = 0", [$memberId])['cnt'] ?? 0);
$children = (int) ($db->selectOne("SELECT COUNT(*) as cnt FROM children WHERE member_id = ? AND is_archived = 0", [$memberId])['cnt'] ?? 0);
$temps = (int) ($db->selectOne("SELECT COUNT(*) as cnt FROM temporary_members WHERE member_id = ? AND is_archived = 0", [$memberId])['cnt'] ?? 0);
return [
'spouses' => $spouses,
'children' => $children,
'temporary' => $temps,
'total' => $spouses + $children + $temps,
];
}
}
This diff is collapsed.
This diff is collapsed.
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE waiver_requests
ADD COLUMN excess_dependent_count INT UNSIGNED NOT NULL DEFAULT 0 AFTER new_dependent_count,
ADD COLUMN excess_fee_percentage DECIMAL(5,2) NULL AFTER excess_dependent_count,
ADD COLUMN excess_fee_amount DECIMAL(15,2) NOT NULL DEFAULT 0.00 AFTER excess_fee_percentage,
ADD COLUMN waiver_request_doc_path VARCHAR(500) NULL AFTER notes,
ADD COLUMN target_form_doc_path VARCHAR(500) NULL AFTER waiver_request_doc_path,
ADD COLUMN debts_cleared TINYINT(1) NOT NULL DEFAULT 0 AFTER annual_renewal_paid;
",
'down' => "
ALTER TABLE waiver_requests
DROP COLUMN excess_dependent_count,
DROP COLUMN excess_fee_percentage,
DROP COLUMN excess_fee_amount,
DROP COLUMN waiver_request_doc_path,
DROP COLUMN target_form_doc_path,
DROP COLUMN debts_cleared;
",
];
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