Commit d92f1d38 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(waiver): auto-complete waiver after fee payment instead of manual approval

After the cashier collects the waiver fee, the system now automatically
executes the waiver completion (membership transfer) without requiring
a manual "إتمام التنازل" button click.

The auto-complete validates all conditions first:
- Fee paid (status = fee_paid)
- Target member specified
- Board approval exists
- No debts on source or target member
- Excess dependent fees properly set

If any condition fails, the waiver stays in fee_paid status with a
clear checklist showing what's still needed, plus a manual fallback
button for edge cases.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 26838f17
......@@ -77,6 +77,78 @@ final class WaiverProcessor
}
}
/**
* Auto-complete a waiver after fee payment — validates all conditions first.
*/
public static function autoComplete(int $waiverId): array
{
$db = App::getInstance()->db();
$waiver = $db->selectOne("SELECT * FROM waiver_requests WHERE id = ?", [$waiverId]);
if (!$waiver || $waiver['status'] !== 'fee_paid') {
return ['success' => false, 'reason' => 'status_invalid'];
}
if (!$waiver['target_member_id']) {
return ['success' => false, 'reason' => 'no_target'];
}
if (!$waiver['approved_by']) {
return ['success' => false, 'reason' => 'not_approved'];
}
$sourceDebtCheck = self::checkDebtsComprehensive((int) $waiver['source_member_id']);
if (!$sourceDebtCheck['clear']) {
return ['success' => false, 'reason' => 'source_debts', 'total' => $sourceDebtCheck['total']];
}
$targetDebtCheck = self::checkDebtsComprehensive((int) $waiver['target_member_id']);
if (!$targetDebtCheck['clear']) {
return ['success' => false, 'reason' => 'target_debts', 'total' => $targetDebtCheck['total']];
}
$originalDeps = [
'spouses' => (int) ($waiver['original_spouses_count'] ?? 0),
'children' => (int) ($waiver['original_children_count'] ?? 0),
'temporary' => (int) ($waiver['original_temporary_count'] ?? 0),
'total' => (int) ($waiver['original_dependent_count'] ?? 0),
];
$targetDeps = self::countDependents((int) $waiver['target_member_id']);
$comparison = self::compareDependents($originalDeps, $targetDeps);
if ($comparison['has_excess']) {
$totalApprovedExcessFee = bcadd(
bcadd($waiver['spouse_fee_total'] ?? '0', $waiver['child_fee_total'] ?? '0', 2),
$waiver['temporary_fee_total'] ?? '0', 2
);
$legacyExcessFee = $waiver['excess_fee_amount'] ?? '0';
if (bccomp($totalApprovedExcessFee, '0', 2) <= 0 && bccomp((string) $legacyExcessFee, '0', 2) <= 0) {
return ['success' => false, 'reason' => 'excess_fees_not_set'];
}
}
$db->update('waiver_requests', [
'new_dependent_count' => $targetDeps['total'],
'new_spouses_count' => $targetDeps['spouses'],
'new_children_count' => $targetDeps['children'],
'new_temporary_count' => $targetDeps['temporary'],
'annual_renewal_paid' => 1,
'debts_cleared' => 1,
'target_debts_cleared' => 1,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$waiverId]);
$result = self::execute($waiverId);
if ($result['success']) {
Logger::info("Waiver #{$waiverId} auto-completed after fee payment", [
'source_member_id' => $waiver['source_member_id'],
'target_member_id' => $waiver['target_member_id'],
]);
}
return $result;
}
/**
* Comprehensive debt check: member + all dependents (spouses, children, temporary).
* Returns detailed breakdown per person.
......
......@@ -4,7 +4,7 @@
<?php
$statusColor = match($waiver['status']) { 'completed' => '#059669', 'fee_paid' => '#2563EB', 'approved' => '#0284C7', 'rejected' => '#DC2626', default => '#D97706' };
$statusLabel = match($waiver['status']) { 'requested' => 'مقدم — في انتظار اعتماد مجلس الأمناء', 'approved' => 'معتمد — في انتظار إرسال للخزينة ودفع الرسوم', 'fee_paid' => '🟢 تم الدفع — جاهز لاعتماد طلب التنازل', 'completed' => 'مكتمل — تم نقل العضوية بنجاح', 'rejected' => 'مرفوض', default => $waiver['status'] };
$statusLabel = match($waiver['status']) { 'requested' => 'مقدم — في انتظار اعتماد مجلس الأمناء', 'approved' => 'معتمد — في انتظار إرسال للخزينة ودفع الرسوم', 'fee_paid' => '🟢 تم الدفع — بانتظار استكمال المتطلبات لاعتماد التنازل تلقائياً', 'completed' => 'مكتمل — تم نقل العضوية بنجاح', 'rejected' => 'مرفوض', default => $waiver['status'] };
$statusIcon = match($waiver['status']) { 'requested' => '⏳', 'approved' => '📋', 'fee_paid' => '🟢', 'completed' => '✅', 'rejected' => '❌', default => '●' };
?>
......@@ -14,7 +14,7 @@ $statusIcon = match($waiver['status']) { 'requested' => '⏳', 'approved' => '
<div style="flex:1;">
<span style="font-weight:700;font-size:16px;color:<?= $statusColor ?>;"><?= $statusLabel ?></span>
<?php if ($waiver['status'] === 'fee_paid'): ?>
<div style="margin-top:4px;font-size:12px;color:#059669;font-weight:600;">جميع الشروط مستوفاة — يمكن الآن إتمام التنازل ونقل العضوية</div>
<div style="margin-top:4px;font-size:12px;color:#059669;font-weight:600;">تم السداد — سيتم اعتماد التنازل تلقائياً فور استيفاء جميع الشروط</div>
<?php endif; ?>
</div>
<?php if ($waiver['approved_by_name']): ?>
......@@ -715,57 +715,55 @@ function updateSummary() {
<?php endif; ?>
<?php endif; ?>
<!-- SECTION 8: Complete (fee_paid status) -->
<?php if ($waiver['status'] === 'fee_paid' && can('waiver.approve')): ?>
<div class="card" style="padding:20px;margin-bottom:20px;background:#ECFDF5;border:2px solid #059669;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">
<span style="font-size:22px;">🟢</span>
<!-- SECTION 8: Fee paid but auto-complete didn't fire (edge case — debts remain or missing target) -->
<?php if ($waiver['status'] === 'fee_paid'): ?>
<div class="card" style="padding:20px;margin-bottom:20px;background:#FEF3C7;border:2px solid #F59E0B;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
<span style="font-size:22px;"></span>
<div>
<h4 style="margin:0;color:#059669;">جاهز لاعتماد طلب التنازل</h4>
<p style="margin:4px 0 0;font-size:12px;color:#166534;">جميع الشروط مستوفاة: الاعتماد ✓ الدفع ✓ عدم وجود مديونيات ✓</p>
<h4 style="margin:0;color:#D97706;">تم السداد — بانتظار استكمال المتطلبات</h4>
<p style="margin:4px 0 0;font-size:12px;color:#92400E;">سيتم اعتماد التنازل تلقائياً فور استيفاء جميع الشروط التالية:</p>
</div>
</div>
<form method="POST" action="/waivers/<?= (int) $waiver['id'] ?>/complete" id="complete-form">
<?= csrf_field() ?>
<ul style="margin:10px 0 0;padding:0 20px;font-size:13px;color:#92400E;list-style:disc;">
<li style="margin-bottom:4px;">✅ سداد رسوم التنازل</li>
<?php if (!$waiver['target_member_id']): ?>
<div style="margin-bottom:15px;">
<label class="form-label">بحث عن المستفيد <span style="color:#DC2626;">*</span></label>
<div style="position:relative;">
<input type="text" id="target-search" class="form-input" placeholder="اكتب اسم أو رقم قومي المستفيد..." autocomplete="off" style="padding-left:40px;">
<i data-lucide="search" style="position:absolute;left:12px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:#9CA3AF;"></i>
</div>
<div id="target-results" style="display:none;position:absolute;z-index:100;background:#fff;border:1px solid #E5E7EB;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);max-height:200px;overflow-y:auto;width:100%;margin-top:4px;"></div>
</div>
<div id="target-selected" style="display:none;padding:12px 16px;background:#F0FDF4;border:1px solid #BBF7D0;border-radius:8px;margin-bottom:15px;">
<div style="display:flex;align-items:center;justify-content:space-between;">
<div>
<span style="font-weight:700;color:#166534;" id="target-name"></span>
<span style="color:#6B7280;font-size:12px;margin-right:10px;" id="target-info"></span>
</div>
<button type="button" onclick="clearTarget()" style="background:none;border:none;color:#DC2626;cursor:pointer;font-size:18px;"></button>
</div>
</div>
<li style="margin-bottom:4px;color:#DC2626;font-weight:700;">❌ لم يتم تحديد المتنازل إليه بعد</li>
<?php else: ?>
<div style="padding:12px 16px;background:#F0FDF4;border:1px solid #BBF7D0;border-radius:8px;margin-bottom:15px;">
<span style="font-weight:700;color:#166534;"><?= e($waiver['target_name'] ?? '') ?></span>
<span style="color:#6B7280;font-size:12px;margin-right:10px;">عضو #<?= (int) $waiver['target_member_id'] ?></span>
</div>
<li style="margin-bottom:4px;">✅ تحديد المتنازل إليه: <?= e($waiver['target_name'] ?? '') ?></li>
<?php endif; ?>
<input type="hidden" name="target_member_id" id="target-member-id" value="<?= (int) ($waiver['target_member_id'] ?? 0) ?>">
<div style="display:flex;gap:10px;align-items:center;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;" onclick="return validateComplete()">✅ إتمام التنازل ونقل العضوية</button>
<?php if (!$waiver['target_member_id']): ?>
<span style="font-size:12px;color:#6B7280;">أو <a href="/members/create" target="_blank" style="color:#0D7377;">أنشئ عضو جديد</a> ثم ارجع هنا</span>
<?php if (!$source_debt_check['clear']): ?>
<li style="margin-bottom:4px;color:#DC2626;font-weight:700;">❌ مديونيات على المتنازل: <?= money($source_debt_check['total']) ?></li>
<?php else: ?>
<li style="margin-bottom:4px;">✅ لا مديونيات على المتنازل</li>
<?php endif; ?>
</div>
<?php if ($target_debt_check && !$target_debt_check['clear']): ?>
<li style="margin-bottom:4px;color:#DC2626;font-weight:700;">❌ مديونيات على المتنازل إليه: <?= money($target_debt_check['total']) ?></li>
<?php elseif ($waiver['target_member_id']): ?>
<li style="margin-bottom:4px;">✅ لا مديونيات على المتنازل إليه</li>
<?php endif; ?>
</ul>
<?php
$allClear = $waiver['target_member_id'] && $source_debt_check['clear'] && (!$target_debt_check || $target_debt_check['clear']);
?>
<?php if ($allClear && can('waiver.approve')): ?>
<div style="margin-top:15px;padding:12px;background:#ECFDF5;border:1px solid #BBF7D0;border-radius:8px;">
<p style="margin:0 0 10px;font-size:13px;color:#166534;font-weight:600;">✅ جميع الشروط مستوفاة — يمكن إتمام التنازل الآن</p>
<form method="POST" action="/waivers/<?= (int) $waiver['id'] ?>/complete" style="display:inline;">
<?= csrf_field() ?>
<input type="hidden" name="target_member_id" value="<?= (int) $waiver['target_member_id'] ?>">
<button type="submit" class="btn btn-primary" style="padding:10px 25px;" onclick="return confirm('إتمام التنازل ونقل العضوية؟')">✅ إتمام التنازل يدوياً</button>
</form>
<span style="font-size:11px;color:#6B7280;margin-right:10px;">(كان يُفترض أن يتم تلقائياً — اضغط هنا لإتمامه يدوياً)</span>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Complete for approved (still needs payment) -->
<!-- Info for approved (still needs payment) -->
<?php if ($waiver['status'] === 'approved' && can('waiver.approve')): ?>
<div style="padding:12px 16px;background:#FEF3C7;border:1px solid #FDE68A;border-radius:8px;margin-bottom:20px;font-size:13px;color:#92400E;">
⏳ لإتمام التنازل: يجب أولاً إرسال طلب الدفع للخزينة وسداد الرسوم، ثم يصبح الطلب جاهزاً لنقل العضوية.
⏳ لإتمام التنازل: يجب إرسال طلب الدفع للخزينة وسداد الرسوم. بعد السداد سيتم اعتماد التنازل ونقل العضوية تلقائياً.
</div>
<?php endif; ?>
......@@ -773,7 +771,7 @@ function updateSummary() {
<a href="/waivers" class="btn btn-outline">← رجوع للقائمة</a>
</div>
<?php if ($waiver['status'] === 'fee_paid' || (!$waiver['target_member_id'] && $waiver['status'] === 'requested')): ?>
<?php if (!$waiver['target_member_id'] && $waiver['status'] === 'requested'): ?>
<script>
(function() {
var searchInput = document.getElementById('target-search');
......
<?php
declare(strict_types=1);
// Waiver module bootstraps under Transfers menu (already registered)
\ No newline at end of file
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Waiver\Services\WaiverProcessor;
// Auto-complete waiver after fee payment
EventBus::listen('waiver.fee_paid', function (array $data): void {
$waiverId = (int) ($data['waiver_id'] ?? 0);
if ($waiverId <= 0) return;
try {
$result = WaiverProcessor::autoComplete($waiverId);
if (!$result['success']) {
Logger::info("Waiver #{$waiverId} auto-complete deferred: " . ($result['reason'] ?? 'unknown'), $data);
}
} catch (\Throwable $e) {
Logger::error("Waiver auto-complete failed for #{$waiverId}: " . $e->getMessage(), $data);
}
});
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