Commit 9c97004c authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix payment void cascade, enhance death workflow, and transfer same-number...

Fix payment void cascade, enhance death workflow, and transfer same-number with companion validation

- Payment void now also voids linked payment_requests and reverts frozen/suspended members
- Death case requires full form fill for spouse, charges 570+annual, supports secondary wives as separate memberships
- Transfers keep the SAME membership number, archive source, charge surcharge for extra companions
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent b4b78902
......@@ -58,14 +58,14 @@ EventBus::listen('payment.voided', function (array $data) {
);
if (!$otherPayment) {
$member = \App\Modules\Members\Models\Member::find($memberId);
if ($member && $member->status === 'active') {
if ($member && in_array($member->status, ['active', 'frozen', 'suspended'], true)) {
$member->update([
'status' => 'payment_pending',
'membership_number' => null,
]);
foreach (['spouses', 'children', 'temporary_members'] as $depTable) {
$db->query(
"UPDATE `{$depTable}` SET status = 'pending_payment', updated_at = NOW() WHERE member_id = ? AND status = 'active' AND is_archived = 0",
"UPDATE `{$depTable}` SET status = 'pending_payment', join_date = NULL, updated_at = NOW() WHERE member_id = ? AND status IN ('active','frozen') AND is_archived = 0",
[$memberId]
);
}
......
......@@ -14,9 +14,21 @@ use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Payments\Services\PaymentService;
use App\Modules\Cashier\Services\PaymentRequestService;
use App\Modules\ServiceCatalog\Models\ServicePrice;
use App\Modules\Members\Models\Member;
class DeathController extends Controller
{
private static function getFees(): array
{
$formFee = ServicePrice::getPrice('SVC_TRANSFER_FORM', '570.00');
$annualSubBase = ServicePrice::getPrice('SVC_ANNUAL_MEMBER', '492.00');
$devFeeData = RuleEngine::get('DEVELOPMENT_FEE');
$devFee = $devFeeData['amount'] ?? '35.00';
$annualSub = bcadd($annualSubBase, $devFee, 2);
$totalFee = bcadd($formFee, $annualSub, 2);
return compact('formFee', 'annualSubBase', 'devFee', 'annualSub', 'totalFee');
}
public function index(Request $request): Response
{
$filters = ['search' => trim((string) $request->get('q', '')), 'status' => $request->get('status', '')];
......@@ -30,22 +42,17 @@ class DeathController extends Controller
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود');
$spouses = $db->select("SELECT * FROM spouses WHERE member_id = ? AND is_archived = 0 AND status = 'active'", [(int) $memberId]);
// Pre-calculate fees to show on form
$formFee = ServicePrice::getPrice('SVC_TRANSFER_FORM', '570.00');
$annualSubBase = ServicePrice::getPrice('SVC_ANNUAL_MEMBER', '492.00');
$devFeeData = RuleEngine::get('DEVELOPMENT_FEE');
$devFee = $devFeeData['amount'] ?? '35.00';
$annualSub = bcadd($annualSubBase, $devFee, 2);
$totalFee = bcadd($formFee, $annualSub, 2);
$spouses = $db->select("SELECT * FROM spouses WHERE member_id = ? AND is_archived = 0 AND status = 'active' ORDER BY spouse_order", [(int) $memberId]);
$children = $db->select("SELECT * FROM children WHERE member_id = ? AND is_archived = 0 ORDER BY child_order", [(int) $memberId]);
$fees = self::getFees();
return $this->view('Death.Views.create', [
'member' => $member,
'spouses' => $spouses,
'form_fee' => $formFee,
'annual_sub' => $annualSub,
'total_fee' => $totalFee,
'children' => $children,
'form_fee' => $fees['formFee'],
'annual_sub' => $fees['annualSub'],
'total_fee' => $fees['totalFee'],
]);
}
......@@ -58,50 +65,78 @@ class DeathController extends Controller
$deceasedType = trim($request->post('deceased_type', ''));
$deathDate = trim($request->post('death_date', ''));
$certNumber = trim($request->post('death_certificate_number', ''));
$spouseId = $request->post('spouse_id') ? (int) $request->post('spouse_id') : null;
$notes = trim($request->post('notes', ''));
$paymentMethod = trim($request->post('payment_method', 'cash'));
if (!$deceasedType || !$deathDate) {
return $this->redirect("/death/create/{$memberId}")->withError('بيانات الوفاة غير مكتملة');
}
// Calculate fee (form fee + annual renewal)
$formFee = ServicePrice::getPrice('SVC_TRANSFER_FORM', '570.00');
$annualSubBase = ServicePrice::getPrice('SVC_ANNUAL_MEMBER', '492.00');
$devFeeData = RuleEngine::get('DEVELOPMENT_FEE');
$devFee = $devFeeData['amount'] ?? '35.00';
$annualSub = bcadd($annualSubBase, $devFee, 2);
$totalFee = bcadd($formFee, $annualSub, 2);
$fees = self::getFees();
$totalFee = $fees['totalFee'];
$caseData = [
'member_id' => (int) $memberId,
'deceased_type' => $deceasedType,
'death_date' => $deathDate,
'death_certificate_number' => $certNumber ?: null,
'same_membership_number' => ($deceasedType === 'primary_member') ? 1 : 0,
'fee_amount' => $totalFee,
'status' => 'recorded',
'notes' => $notes ?: null,
];
if ($deceasedType === 'primary_member') {
$primarySpouseId = $request->post('primary_spouse_id') ? (int) $request->post('primary_spouse_id') : null;
$secondarySpouseIds = $request->post('secondary_spouse_ids', []);
$childrenAssignment = $request->post('children_assignment', []);
if (!$primarySpouseId) {
return $this->redirect("/death/create/{$memberId}")->withError('يجب اختيار الزوجة الأساسية لنقل العضوية');
}
$case = DeathCase::create([
'member_id' => (int) $memberId,
'deceased_type' => $deceasedType,
'death_date' => $deathDate,
'death_certificate_number' => $certNumber ?: null,
'spouse_id' => $spouseId,
'same_membership_number' => ($deceasedType === 'primary_member') ? 1 : 0,
'fee_amount' => $totalFee,
'status' => 'recorded',
'notes' => $notes ?: null,
]);
$caseData['spouse_id'] = $primarySpouseId;
$caseData['primary_spouse_id'] = $primarySpouseId;
$caseData['secondary_spouses_json'] = !empty($secondarySpouseIds) ? json_encode(array_map('intval', $secondarySpouseIds)) : null;
$caseData['children_assignment_json'] = !empty($childrenAssignment) ? json_encode($childrenAssignment) : null;
// Total fee: 570+annual for primary + 570+annual for each secondary
$secondaryCount = count($secondarySpouseIds);
if ($secondaryCount > 0) {
$totalFee = bcmul($fees['totalFee'], (string) (1 + $secondaryCount), 2);
$caseData['fee_amount'] = $totalFee;
}
} elseif ($deceasedType === 'spouse') {
$spouseId = $request->post('spouse_id') ? (int) $request->post('spouse_id') : null;
$caseData['spouse_id'] = $spouseId;
$caseData['fee_amount'] = '0.00';
} elseif ($deceasedType === 'child') {
$childId = $request->post('child_id') ? (int) $request->post('child_id') : null;
$caseData['child_id'] = $childId;
$caseData['fee_amount'] = '0.00';
}
$case = DeathCase::create($caseData);
EventBus::dispatch('death.recorded', ['case_id' => (int) $case->id, 'member_id' => (int) $memberId, 'type' => $deceasedType]);
// Send payment to cashier queue
// Send payment to cashier queue (only for primary_member death which requires transfer fee)
if (bccomp($totalFee, '0', 2) > 0) {
$deceasedLabel = match ($deceasedType) {
'primary_member' => 'العضو الأصلي',
'spouse' => 'الزوج/ة',
default => $deceasedType,
};
$secondaryCount = 0;
if (!empty($caseData['secondary_spouses_json'])) {
$secondaryCount = count(json_decode($caseData['secondary_spouses_json'], true));
}
$breakdown = [
'📋 نوع الوفاة: ' . $deceasedLabel,
'📝 رسوم استمارة نقل: ' . money($formFee),
'📅 اشتراك سنوي: ' . money($annualSubBase) . ' + تنمية ' . money($devFee) . ' = ' . money($annualSub),
'═══════════════════════════',
'💵 الإجمالي: ' . money($totalFee),
'رسوم نقل العضوية للزوجة الأساسية:',
' رسوم استمارة (570): ' . money($fees['formFee']),
' اشتراك سنوي: ' . money($fees['annualSub']),
];
if ($secondaryCount > 0) {
$breakdown[] = '';
$breakdown[] = "رسوم عضويات منفصلة ({$secondaryCount} زوجة إضافية):";
$breakdown[] = " {$secondaryCount} × " . money($fees['totalFee']) . ' = ' . money(bcmul($fees['totalFee'], (string) $secondaryCount, 2));
}
$breakdown[] = '═══════════════════════════';
$breakdown[] = 'الإجمالي: ' . money($totalFee);
$result = PaymentRequestService::createRequest([
'member_id' => (int) $memberId,
......@@ -109,19 +144,15 @@ class DeathController extends Controller
'payment_type' => 'death_fee',
'related_entity_type' => 'death_cases',
'related_entity_id' => (int) $case->id,
'description_ar' => 'رسوم وفاة (استمارة + اشتراك) — حالة #' . $case->id,
'description_ar' => 'رسوم وفاة — نقل عضوية — حالة #' . $case->id,
'notes' => json_encode(['fee_breakdown' => $breakdown], JSON_UNESCAPED_UNICODE),
]);
if ($result['success']) {
return $this->redirect("/death/{$case->id}")->withSuccess(
'تم تسجيل حالة الوفاة وإرسال طلب الدفع للخزينة — ' . money($totalFee) . ' — رقم الطلب: ' . $result['request_number']
'تم تسجيل حالة الوفاة وإرسال طلب الدفع للخزينة — ' . money($totalFee)
);
}
return $this->redirect("/death/{$case->id}")->withError(
'تم تسجيل الحالة لكن فشل إنشاء طلب الدفع: ' . ($result['error'] ?? 'خطأ غير معروف')
);
}
return $this->redirect("/death/{$case->id}")->withSuccess('تم تسجيل حالة الوفاة');
......@@ -137,19 +168,31 @@ class DeathController extends Controller
);
if (!$case) return $this->redirect('/death')->withError('الحالة غير موجودة');
// Compute fee breakdown components
$formFee = ServicePrice::getPrice('SVC_TRANSFER_FORM', '570.00');
$annualSubBase = ServicePrice::getPrice('SVC_ANNUAL_MEMBER', '492.00');
$devFeeData = RuleEngine::get('DEVELOPMENT_FEE');
$devFee = $devFeeData['amount'] ?? '35.00';
$annualSub = bcadd($annualSubBase, $devFee, 2);
$fees = self::getFees();
$primarySpouse = null;
$secondarySpouses = [];
$newMember = null;
if ($case['primary_spouse_id']) {
$primarySpouse = $db->selectOne("SELECT * FROM spouses WHERE id = ?", [(int) $case['primary_spouse_id']]);
}
if (!empty($case['secondary_spouses_json'])) {
$ids = json_decode($case['secondary_spouses_json'], true);
if ($ids) {
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$secondarySpouses = $db->select("SELECT * FROM spouses WHERE id IN ({$placeholders})", $ids);
}
}
if ($case['transferred_to_member_id']) {
$newMember = $db->selectOne("SELECT id, full_name_ar, membership_number, status FROM members WHERE id = ?", [(int) $case['transferred_to_member_id']]);
}
return $this->view('Death.Views.show', [
'case' => $case,
'formFee' => $formFee,
'annualSubBase' => $annualSubBase,
'devFee' => $devFee,
'annualSub' => $annualSub,
'case' => $case,
'fees' => $fees,
'primarySpouse' => $primarySpouse,
'secondarySpouses' => $secondarySpouses,
'newMember' => $newMember,
]);
}
......@@ -158,7 +201,7 @@ class DeathController extends Controller
$db = App::getInstance()->db();
$case = $db->selectOne("SELECT * FROM death_cases WHERE id = ?", [(int) $id]);
if (!$case) return $this->redirect('/death')->withError('الحالة غير موجودة');
if ($case['status'] === 'completed' || $case['status'] === 'fee_paid') {
if (in_array($case['status'], ['completed', 'fee_paid', 'pending_form_fill'], true)) {
return $this->redirect("/death/{$id}")->withError('تم دفع الرسوم مسبقاً');
}
......@@ -174,23 +217,79 @@ class DeathController extends Controller
'payment_method' => $request->post('payment_method', 'cash'),
'related_entity_type' => 'death_cases',
'related_entity_id' => (int) $id,
'description' => 'رسوم وفاة (استمارة + اشتراك) — حالة #' . $id,
'description' => 'رسوم وفاة — حالة #' . $id,
]);
if (!$result['success']) {
return $this->redirect("/death/{$id}")->withError($result['error']);
}
// For primary member death: move to pending_form_fill so wife fills full form
$newStatus = ($case['deceased_type'] === 'primary_member') ? 'pending_form_fill' : 'fee_paid';
$db->update('death_cases', [
'status' => 'fee_paid',
'status' => $newStatus,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
EventBus::dispatch('death.fee_paid', ['case_id' => (int) $id, 'payment_id' => $result['payment_id']]);
if ($newStatus === 'pending_form_fill') {
return $this->redirect("/death/{$id}")->withSuccess('تم دفع الرسوم — الآن يجب ملء استمارة العضوية الجديدة للزوجة');
}
return $this->redirect("/death/{$id}")->withSuccess('تم دفع الرسوم — إيصال: ' . $result['receipt_number']);
}
public function fillForm(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$case = $db->selectOne("SELECT * FROM death_cases WHERE id = ?", [(int) $id]);
if (!$case || $case['status'] !== 'pending_form_fill') {
return $this->redirect("/death/{$id}")->withError('لا يمكن ملء الاستمارة في هذه المرحلة');
}
$spouse = $db->selectOne("SELECT * FROM spouses WHERE id = ?", [(int) $case['primary_spouse_id']]);
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $case['member_id']]);
return $this->view('Death.Views.form_fill', [
'case' => $case,
'spouse' => $spouse,
'member' => $member,
'qualifications' => $db->select("SELECT id, name_ar FROM qualifications WHERE is_active = 1 ORDER BY sort_order"),
'governorates' => $db->select("SELECT code, name_ar FROM governorates WHERE is_active = 1 ORDER BY name_ar"),
'countries' => $db->select("SELECT nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar"),
]);
}
public function saveFillForm(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$case = $db->selectOne("SELECT * FROM death_cases WHERE id = ?", [(int) $id]);
if (!$case || $case['status'] !== 'pending_form_fill') {
return $this->redirect("/death/{$id}")->withError('لا يمكن ملء الاستمارة في هذه المرحلة');
}
$data = $request->all();
unset($data['_csrf_token']);
$requiredFields = ['full_name_ar', 'national_id', 'date_of_birth', 'gender', 'qualification_id'];
foreach ($requiredFields as $field) {
if (empty($data[$field])) {
return $this->redirect("/death/{$id}/fill-form")->withError("الحقل {$field} مطلوب");
}
}
// Store form data in session for completion step
$db->update('death_cases', [
'primary_spouse_form_filled' => 1,
'notes' => json_encode(['form_data' => $data], JSON_UNESCAPED_UNICODE),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
return $this->redirect("/death/{$id}")->withSuccess('تم ملء الاستمارة — يمكن الآن إتمام نقل العضوية');
}
public function complete(Request $request, string $id): Response
{
$db = App::getInstance()->db();
......@@ -204,68 +303,159 @@ class DeathController extends Controller
$snapshotId = ArchiveService::takeSnapshot('members', (int) $case['member_id'], 'death', 'وفاة — حالة #' . $id);
if ($case['deceased_type'] === 'spouse' && $case['spouse_id']) {
// Spouse death: archive spouse
$db->update('spouses', [
'status' => 'deceased', 'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $case['spouse_id']]);
} elseif ($case['deceased_type'] === 'child' && $case['child_id']) {
$db->update('children', [
'status' => 'deceased', 'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $case['child_id']]);
} elseif ($case['deceased_type'] === 'primary_member') {
// Primary member death: transfer to spouse with SAME number
if (!$case['primary_spouse_form_filled']) {
$db->rollBack();
return $this->redirect("/death/{$id}")->withError('يجب ملء استمارة العضوية الجديدة أولاً');
}
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $case['member_id']]);
$spouse = $case['spouse_id'] ? $db->selectOne("SELECT * FROM spouses WHERE id = ?", [(int) $case['spouse_id']]) : null;
if ($spouse) {
// Create new member from spouse data with SAME membership number
$newMemberId = $db->insert('members', [
'membership_number' => $member['membership_number'], // SAME number
'full_name_ar' => $spouse['full_name_ar'],
'national_id' => $spouse['national_id'],
'date_of_birth' => $spouse['date_of_birth'],
'age_years' => $spouse['age_years'],
'gender' => $spouse['gender'],
'nationality' => $spouse['nationality'] ?? 'مصري',
'branch_id' => (int) $member['branch_id'],
'membership_type' => 'working',
'status' => 'active',
'phone_mobile' => $spouse['mobile'] ?? $member['phone_mobile'],
'membership_value' => $member['membership_value'],
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
]);
// Archive old member
$db->update('members', [
'is_archived' => 1, 'archived_at' => date('Y-m-d H:i:s'),
'status' => 'deceased', 'membership_number' => null,
], '`id` = ?', [(int) $case['member_id']]);
// Archive spouse record
$db->update('spouses', [
'status' => 'transferred', 'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $case['spouse_id']]);
// Transfer children to new member
$spouse = $db->selectOne("SELECT * FROM spouses WHERE id = ?", [(int) $case['primary_spouse_id']]);
// Extract form data stored during fill-form step
$formData = [];
if (!empty($case['notes'])) {
$notesData = json_decode($case['notes'], true);
$formData = $notesData['form_data'] ?? [];
}
// Create new member from form data with SAME membership number
$newMemberData = [
'membership_number' => $member['membership_number'],
'full_name_ar' => $formData['full_name_ar'] ?? $spouse['full_name_ar'],
'full_name_en' => $formData['full_name_en'] ?? $spouse['full_name_en'] ?? null,
'national_id' => $formData['national_id'] ?? $spouse['national_id'],
'date_of_birth' => $formData['date_of_birth'] ?? $spouse['date_of_birth'],
'gender' => $formData['gender'] ?? $spouse['gender'],
'nationality' => $formData['nationality'] ?? $spouse['nationality'] ?? 'مصري',
'branch_id' => (int) $member['branch_id'],
'membership_type' => 'working',
'member_category' => 'working_member',
'status' => 'active',
'qualification_id' => !empty($formData['qualification_id']) ? (int) $formData['qualification_id'] : ($spouse['qualification_id'] ?? $member['qualification_id']),
'phone_mobile' => $formData['phone_mobile'] ?? $spouse['mobile'] ?? $member['phone_mobile'],
'membership_value' => $member['membership_value'],
'transferred_from_divorce_id' => null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
];
// Add optional form fields
$optionalFields = ['marital_status', 'religion', 'phone_home', 'email', 'emergency_name', 'emergency_phone', 'residence_address', 'area', 'governorate', 'occupation', 'job_title'];
foreach ($optionalFields as $field) {
if (!empty($formData[$field])) {
$newMemberData[$field] = $formData[$field];
}
}
$newMemberId = $db->insert('members', $newMemberData);
// Archive old member
$db->update('members', [
'is_archived' => 1, 'archived_at' => date('Y-m-d H:i:s'),
'status' => 'deceased', 'membership_number' => null,
], '`id` = ?', [(int) $case['member_id']]);
// Archive primary spouse record
$db->update('spouses', [
'status' => 'transferred', 'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $case['primary_spouse_id']]);
// Handle children assignment
$childrenAssignment = !empty($case['children_assignment_json']) ? json_decode($case['children_assignment_json'], true) : [];
if (!empty($childrenAssignment)) {
foreach ($childrenAssignment as $childId => $assignTo) {
if ($assignTo === 'primary') {
$db->update('children', ['member_id' => $newMemberId, 'updated_at' => date('Y-m-d H:i:s')],
'`id` = ? AND `is_archived` = 0', [(int) $childId]);
}
// Children assigned to secondary spouses will be moved when secondary memberships are created
}
} else {
// Default: all children go to primary
$db->update('children', ['member_id' => $newMemberId, 'updated_at' => date('Y-m-d H:i:s')],
'`member_id` = ? AND `is_archived` = 0', [(int) $case['member_id']]);
}
// Record number chain
ArchiveService::recordNumberTransfer($member['membership_number'], 'death_transfer', 'members', $newMemberId);
// Handle secondary spouses — create separate new memberships
if (!empty($case['secondary_spouses_json'])) {
$secondaryIds = json_decode($case['secondary_spouses_json'], true);
foreach ($secondaryIds as $secSpouseId) {
$secSpouse = $db->selectOne("SELECT * FROM spouses WHERE id = ?", [(int) $secSpouseId]);
if ($secSpouse) {
$secMemberId = $db->insert('members', [
'full_name_ar' => $secSpouse['full_name_ar'],
'full_name_en' => $secSpouse['full_name_en'] ?? null,
'national_id' => $secSpouse['national_id'],
'date_of_birth' => $secSpouse['date_of_birth'],
'gender' => $secSpouse['gender'],
'nationality' => $secSpouse['nationality'] ?? 'مصري',
'branch_id' => (int) $member['branch_id'],
'membership_type' => 'working',
'member_category' => 'working_member',
'status' => 'form_pending',
'phone_mobile' => $secSpouse['mobile'] ?? null,
'membership_value' => $member['membership_value'],
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
]);
// Assign new membership number for secondary
\App\Modules\Members\Services\MemberNumberGenerator::assign($secMemberId);
// Archive secondary spouse
$db->update('spouses', [
'status' => 'transferred', 'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $secSpouseId]);
// Move children assigned to this spouse
if (!empty($childrenAssignment)) {
foreach ($childrenAssignment as $childId => $assignTo) {
if ((int) $assignTo === (int) $secSpouseId) {
$db->update('children', ['member_id' => $secMemberId, 'updated_at' => date('Y-m-d H:i:s')],
'`id` = ? AND `is_archived` = 0', [(int) $childId]);
}
}
}
}
}
}
$db->update('death_cases', [
'transferred_to_member_id' => $newMemberId,
'archive_snapshot_id' => $snapshotId,
'status' => 'completed',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
// Also transfer temporary_members to new primary
$db->update('temporary_members', ['member_id' => $newMemberId, 'updated_at' => date('Y-m-d H:i:s')],
'`member_id` = ? AND `is_archived` = 0', [(int) $case['member_id']]);
$db->commit();
EventBus::dispatch('death.completed', ['case_id' => (int) $id, 'new_member_id' => $newMemberId]);
return $this->redirect("/death/{$id}")->withSuccess('تم نقل العضوية للزوج/ة بنفس رقم العضوية');
}
// Record number chain
ArchiveService::recordNumberTransfer($member['membership_number'], 'death_transfer', 'members', $newMemberId);
$db->update('death_cases', [
'transferred_to_member_id' => $newMemberId,
'archive_snapshot_id' => $snapshotId,
'status' => 'completed',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
$db->commit();
EventBus::dispatch('death.completed', ['case_id' => (int) $id, 'new_member_id' => $newMemberId]);
return $this->redirect("/death/{$id}")->withSuccess('تم نقل العضوية بنفس الرقم — رقم العضوية: ' . $member['membership_number']);
}
// Non-primary-member death (spouse or child)
$db->update('death_cases', [
'archive_snapshot_id' => $snapshotId,
'status' => 'completed',
......@@ -279,4 +469,4 @@ class DeathController extends Controller
return $this->redirect("/death/{$id}")->withError('فشل: ' . $e->getMessage());
}
}
}
\ No newline at end of file
}
......@@ -18,7 +18,9 @@ class DeathCase extends Model
protected static array $fillable = [
'member_id', 'deceased_type', 'death_date',
'death_certificate_number', 'death_certificate_path',
'spouse_id', 'child_id', 'transferred_to_member_id',
'spouse_id', 'primary_spouse_id', 'secondary_spouses_json',
'primary_spouse_form_filled', 'child_id',
'children_assignment_json', 'transferred_to_member_id',
'same_membership_number', 'fee_amount',
'archive_snapshot_id', 'workflow_instance_id', 'status', 'notes',
];
......
......@@ -2,10 +2,12 @@
declare(strict_types=1);
return [
['GET', '/death', 'Death\Controllers\DeathController@index', ['auth'], 'transfer.view'],
['GET', '/death/create/{memberId}', 'Death\Controllers\DeathController@create', ['auth'], 'transfer.initiate'],
['POST', '/death/store/{memberId}', 'Death\Controllers\DeathController@store', ['auth', 'csrf'], 'transfer.initiate'],
['GET', '/death/{id}', 'Death\Controllers\DeathController@show', ['auth'], 'transfer.view'],
['POST', '/death/{id}/pay', 'Death\Controllers\DeathController@pay', ['auth', 'csrf'], 'payment.collect'],
['POST', '/death/{id}/complete', 'Death\Controllers\DeathController@complete', ['auth', 'csrf'], 'transfer.approve'],
['GET', '/death', 'Death\Controllers\DeathController@index', ['auth'], 'transfer.view'],
['GET', '/death/create/{memberId}', 'Death\Controllers\DeathController@create', ['auth'], 'transfer.initiate'],
['POST', '/death/store/{memberId}', 'Death\Controllers\DeathController@store', ['auth', 'csrf'], 'transfer.initiate'],
['GET', '/death/{id}', 'Death\Controllers\DeathController@show', ['auth'], 'transfer.view'],
['GET', '/death/{id}/fill-form', 'Death\Controllers\DeathController@fillForm', ['auth'], 'transfer.initiate'],
['POST', '/death/{id}/fill-form', 'Death\Controllers\DeathController@saveFillForm', ['auth', 'csrf'], 'transfer.initiate'],
['POST', '/death/{id}/pay', 'Death\Controllers\DeathController@pay', ['auth', 'csrf'], 'payment.collect'],
['POST', '/death/{id}/complete', 'Death\Controllers\DeathController@complete', ['auth', 'csrf'], 'transfer.approve'],
];
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تسجيل حالة وفاة — <?= e($member['full_name_ar']) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:15px;padding:15px;display:flex;justify-content:space-between;align-items:center;">
<div>
<strong>العضو:</strong> <?= e($member['full_name_ar']) ?>
&nbsp;|&nbsp; <strong>رقم العضوية:</strong> <?= e($member['membership_number'] ?? 'لم يُحدد') ?>
</div>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">← العودة للعضو</a>
</div>
<form method="POST" action="/death/store/<?= (int) $member['id'] ?>">
<?= csrf_field() ?>
<div class="card" style="padding:20px;margin-bottom:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;"><h3 style="margin:0;color:#1F2937;">بيانات الوفاة</h3></div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">المتوفى <span style="color:#DC2626;">*</span></label>
<select name="deceased_type" class="form-select" required>
<select name="deceased_type" id="deceased_type" class="form-select" required>
<option value="">-- اختر --</option>
<option value="primary_member">العضو الرئيسي (<?= e($member['full_name_ar']) ?>)</option>
<?php foreach ($spouses as $s): ?><option value="spouse" data-spouse="<?= (int) $s['id'] ?>">الزوج/ة (<?= e($s['full_name_ar']) ?>)</option><?php endforeach; ?>
<?php foreach ($spouses as $s): ?>
<option value="spouse" data-spouse-id="<?= (int) $s['id'] ?>">الزوج/ة: <?= e($s['full_name_ar']) ?></option>
<?php endforeach; ?>
<?php foreach ($children as $c): ?>
<option value="child" data-child-id="<?= (int) $c['id'] ?>">ابن/ابنة: <?= e($c['full_name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group"><label class="form-label">تاريخ الوفاة <span style="color:#DC2626;">*</span></label><input type="date" name="death_date" class="form-input" required max="<?= e(date('Y-m-d')) ?>"></div>
<div class="form-group"><label class="form-label">رقم شهادة الوفاة</label><input type="text" name="death_certificate_number" class="form-input"></div>
<?php if (!empty($spouses)): ?>
<div class="form-group"><label class="form-label">الزوج/ة (لنقل العضوية)</label>
<select name="spouse_id" class="form-select"><option value="">-- اختر --</option>
<?php foreach ($spouses as $s): ?><option value="<?= (int) $s['id'] ?>"><?= e($s['full_name_ar']) ?></option><?php endforeach; ?>
</select>
<div class="form-group">
<label class="form-label">تاريخ الوفاة <span style="color:#DC2626;">*</span></label>
<input type="date" name="death_date" class="form-input" required max="<?= e(date('Y-m-d')) ?>">
</div>
<?php endif; ?>
<div class="form-group" style="grid-column:1/-1;"><label class="form-label">ملاحظات</label><textarea name="notes" class="form-textarea" rows="3"></textarea></div>
<div class="form-group">
<label class="form-label">رقم شهادة الوفاة</label>
<input type="text" name="death_certificate_number" class="form-input">
</div>
<input type="hidden" name="spouse_id" id="hidden_spouse_id">
<input type="hidden" name="child_id" id="hidden_child_id">
</div>
</div>
<!-- Fee & Payment -->
<div class="card" style="padding:20px;margin-bottom:20px;background:#FFF7ED;border:2px solid #F59E0B;">
<h4 style="margin:0 0 15px;color:#D97706;">رسوم الإجراء (استمارة + اشتراك سنوي)</h4>
<table style="font-size:14px;margin-bottom:15px;">
<tr><td style="padding:4px 20px 4px 0;color:#6B7280;">رسوم استمارة نقل</td><td style="font-weight:600;"><?= money($form_fee) ?></td></tr>
<tr><td style="padding:4px 20px 4px 0;color:#6B7280;">اشتراك سنوي (492 + 35 تنمية)</td><td style="font-weight:600;"><?= money($annual_sub) ?></td></tr>
<tr style="border-top:2px solid #D97706;"><td style="padding:8px 20px 4px 0;font-weight:700;">الإجمالي</td><td style="font-weight:700;font-size:18px;color:#DC2626;"><?= money($total_fee) ?></td></tr>
</table>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">طريقة الدفع <span style="color:#DC2626;">*</span></label>
<select name="payment_method" class="form-select" required>
<option value="cash">نقدي</option>
<option value="visa">فيزا</option>
<option value="bank_transfer">تحويل بنكي</option>
</select>
<!-- Primary member death: spouse selection + children assignment -->
<div id="primary-death-section" style="display:none;">
<?php if (count($spouses) > 0): ?>
<div class="card" style="margin-bottom:20px;border:2px solid #0D7377;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;background:#F0FDFA;">
<h3 style="margin:0;color:#0D7377;font-size:15px;">نقل العضوية — اختيار الزوجة الأساسية</h3>
<p style="margin:5px 0 0;font-size:12px;color:#6B7280;">الزوجة الأساسية ترث نفس رقم العضوية وتملأ استمارة جديدة (570 + اشتراك سنوي)</p>
</div>
<div style="padding:20px;">
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">الزوجة الأساسية (تأخذ نفس رقم العضوية) <span style="color:#DC2626;">*</span></label>
<select name="primary_spouse_id" id="primary_spouse_id" class="form-select">
<option value="">-- اختر الزوجة الأولى --</option>
<?php foreach ($spouses as $s): ?>
<option value="<?= (int) $s['id'] ?>"><?= e($s['full_name_ar']) ?> (ترتيب: <?= (int) $s['spouse_order'] ?>)</option>
<?php endforeach; ?>
</select>
</div>
<?php if (count($spouses) > 1): ?>
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">الزوجات الإضافيات (عضويات منفصلة — 570 + اشتراك لكل واحدة)</label>
<?php foreach ($spouses as $s): ?>
<label style="display:flex;align-items:center;gap:8px;padding:5px 0;font-size:14px;" class="secondary-spouse-label" data-id="<?= (int) $s['id'] ?>">
<input type="checkbox" name="secondary_spouse_ids[]" value="<?= (int) $s['id'] ?>" class="secondary-spouse-cb">
<?= e($s['full_name_ar']) ?>
</label>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php if (count($children) > 0): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#1F2937;font-size:15px;">توزيع الأبناء</h3>
<p style="margin:5px 0 0;font-size:12px;color:#6B7280;">حدد لأي عضوية ينتقل كل ابن/ابنة (الافتراضي: الزوجة الأساسية)</p>
</div>
<div style="padding:20px;">
<?php foreach ($children as $c): ?>
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid #F3F4F6;">
<span style="font-weight:600;min-width:150px;"><?= e($c['full_name_ar']) ?></span>
<select name="children_assignment[<?= (int) $c['id'] ?>]" class="form-select" style="max-width:250px;">
<option value="primary">الزوجة الأساسية</option>
<?php foreach ($spouses as $s): ?>
<option value="<?= (int) $s['id'] ?>"><?= e($s['full_name_ar']) ?> (منفصلة)</option>
<?php endforeach; ?>
</select>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<button type="submit" class="btn btn-primary" onclick="return confirm('سيتم تسجيل حالة الوفاة وتحصيل الرسوم. متأكد؟')">تسجيل الوفاة وتحصيل الرسوم</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
<!-- Fee Summary -->
<div id="fee-section" style="display:none;">
<div class="card" style="padding:20px;margin-bottom:20px;background:#FFF7ED;border:2px solid #F59E0B;">
<h4 style="margin:0 0 15px;color:#D97706;">رسوم الإجراء</h4>
<table style="font-size:14px;margin-bottom:15px;" id="fee-table">
<tr><td style="padding:4px 20px 4px 0;color:#6B7280;">رسوم استمارة (570)</td><td style="font-weight:600;"><?= money($form_fee) ?></td></tr>
<tr><td style="padding:4px 20px 4px 0;color:#6B7280;">اشتراك سنوي</td><td style="font-weight:600;"><?= money($annual_sub) ?></td></tr>
<tr style="border-top:2px solid #D97706;" id="fee-total-row"><td style="padding:8px 20px 4px 0;font-weight:700;">الإجمالي</td><td style="font-weight:700;font-size:18px;color:#DC2626;"><?= money($total_fee) ?></td></tr>
</table>
</div>
</div>
<div class="form-group" style="margin-bottom:20px;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-textarea" rows="2"></textarea>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary" onclick="return confirm('سيتم تسجيل حالة الوفاة. متأكد؟')">تسجيل حالة الوفاة</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</div>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->endSection(); ?>
<?php $__template->section('scripts'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
var deceasedType = document.getElementById('deceased_type');
var primarySection = document.getElementById('primary-death-section');
var feeSection = document.getElementById('fee-section');
var hiddenSpouse = document.getElementById('hidden_spouse_id');
var hiddenChild = document.getElementById('hidden_child_id');
var primarySelect = document.getElementById('primary_spouse_id');
deceasedType.addEventListener('change', function() {
var selected = this.options[this.selectedIndex];
hiddenSpouse.value = selected.dataset.spouseId || '';
hiddenChild.value = selected.dataset.childId || '';
if (this.value === 'primary_member') {
primarySection.style.display = 'block';
feeSection.style.display = 'block';
} else {
primarySection.style.display = 'none';
feeSection.style.display = (this.value === 'spouse' || this.value === 'child') ? 'none' : 'block';
}
});
// Hide secondary spouse checkbox that matches primary selection
if (primarySelect) {
primarySelect.addEventListener('change', function() {
var primaryId = this.value;
document.querySelectorAll('.secondary-spouse-label').forEach(function(label) {
if (label.dataset.id === primaryId) {
label.style.display = 'none';
label.querySelector('input').checked = false;
} else {
label.style.display = 'flex';
}
});
});
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>استمارة عضوية جديدة — نقل وفاة #<?= (int) $case['id'] ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:15px;padding:15px;background:#F0FDFA;border:2px solid #0D7377;">
<strong style="color:#0D7377;">نقل عضوية بسبب وفاة</strong>
الزوجة <?= e($spouse['full_name_ar'] ?? '—') ?> ترث العضوية رقم <?= e($member['membership_number'] ?? '—') ?>
<br><small style="color:#6B7280;">يجب ملء جميع البيانات المطلوبة لاستمارة العضوية الجديدة</small>
</div>
<form method="POST" action="/death/<?= (int) $case['id'] ?>/fill-form">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;"><h3 style="margin:0;color:#0D7377;">البيانات الشخصية</h3></div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">الاسم بالكامل (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="full_name_ar" value="<?= e($spouse['full_name_ar'] ?? '') ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="full_name_en" value="<?= e($spouse['full_name_en'] ?? '') ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">الرقم القومي <span style="color:#DC2626;">*</span></label>
<input type="text" name="national_id" value="<?= e($spouse['national_id'] ?? '') ?>" class="form-input" required maxlength="14">
</div>
<div class="form-group">
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label>
<input type="date" name="date_of_birth" value="<?= e($spouse['date_of_birth'] ?? '') ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="gender" class="form-select" required>
<option value="male" <?= ($spouse['gender'] ?? '') === 'male' ? 'selected' : '' ?>>ذكر</option>
<option value="female" <?= ($spouse['gender'] ?? '') === 'female' ? 'selected' : '' ?>>أنثى</option>
</select>
</div>
<div class="form-group">
<label class="form-label">الجنسية</label>
<select name="nationality" class="form-select">
<option value="مصري">مصري</option>
<?php foreach ($countries as $c): ?>
<?php if ($c['nationality_ar'] !== 'مصري'): ?>
<option value="<?= e($c['nationality_ar']) ?>" <?= ($spouse['nationality'] ?? '') === $c['nationality_ar'] ? 'selected' : '' ?>><?= e($c['nationality_ar']) ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الديانة</label>
<select name="religion" class="form-select">
<option value="">-- اختر --</option>
<option value="مسلم">مسلم</option>
<option value="مسيحي">مسيحي</option>
</select>
</div>
<div class="form-group">
<label class="form-label">الحالة الاجتماعية</label>
<select name="marital_status" class="form-select">
<option value="">-- اختر --</option>
<option value="أرملة" selected>أرملة</option>
<option value="متزوج">متزوج/ة</option>
<option value="أعزب">أعزب</option>
</select>
</div>
<div class="form-group">
<label class="form-label">المؤهل <span style="color:#DC2626;">*</span></label>
<select name="qualification_id" class="form-select" required>
<option value="">-- اختر المؤهل --</option>
<?php foreach ($qualifications as $q): ?>
<option value="<?= (int) $q['id'] ?>"><?= e($q['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;"><h3 style="margin:0;color:#0D7377;">بيانات الاتصال</h3></div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">الهاتف المحمول</label>
<input type="text" name="phone_mobile" value="<?= e($spouse['mobile'] ?? '') ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">هاتف المنزل</label>
<input type="text" name="phone_home" class="form-input">
</div>
<div class="form-group">
<label class="form-label">البريد الإلكتروني</label>
<input type="email" name="email" class="form-input">
</div>
<div class="form-group">
<label class="form-label">اسم شخص للطوارئ</label>
<input type="text" name="emergency_name" class="form-input">
</div>
<div class="form-group">
<label class="form-label">هاتف الطوارئ</label>
<input type="text" name="emergency_phone" class="form-input">
</div>
</div>
</div>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;"><h3 style="margin:0;color:#0D7377;">العنوان والعمل</h3></div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">عنوان الإقامة</label>
<input type="text" name="residence_address" class="form-input">
</div>
<div class="form-group">
<label class="form-label">المنطقة</label>
<input type="text" name="area" class="form-input">
</div>
<div class="form-group">
<label class="form-label">المحافظة</label>
<select name="governorate" class="form-select">
<option value="">-- اختر --</option>
<?php foreach ($governorates as $g): ?>
<option value="<?= e($g['name_ar']) ?>"><?= e($g['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الوظيفة</label>
<input type="text" name="occupation" class="form-input">
</div>
<div class="form-group">
<label class="form-label">المسمى الوظيفي</label>
<input type="text" name="job_title" class="form-input">
</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">حفظ الاستمارة</button>
<a href="/death/<?= (int) $case['id'] ?>" class="btn btn-outline">← العودة</a>
</div>
</form>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حالة وفاة #<?= (int) $case['id'] ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusLabels = [
'recorded' => 'مسجّل — في انتظار الدفع',
'fee_paid' => 'تم الدفع',
'pending_form_fill' => 'في انتظار ملء الاستمارة',
'completed' => 'مكتمل',
];
$statusColors = [
'recorded' => '#D97706',
'fee_paid' => '#2563EB',
'pending_form_fill' => '#7C3AED',
'completed' => '#059669',
];
$statusLabel = $statusLabels[$case['status']] ?? $case['status'];
$statusColor = $statusColors[$case['status']] ?? '#6B7280';
$deceasedLabel = match($case['deceased_type']) {
'primary_member' => 'العضو الرئيسي',
'spouse' => 'الزوج/ة',
'child' => 'ابن/ابنة',
default => $case['deceased_type'],
};
?>
<!-- Case Info -->
<div class="card" style="padding:20px;margin-bottom:20px;">
<table style="width:100%;max-width:600px;font-size:14px;">
<tr><td style="padding:6px 0;color:#6B7280;width:35%;">العضو</td><td style="padding:6px 0;font-weight:600;"><?= e($case['member_name']) ?> (<?= e($case['membership_number'] ?? '—') ?>)</td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">المتوفى</td><td style="padding:6px 0;font-weight:600;"><?= $case['deceased_type'] === 'primary_member' ? 'العضو الرئيسي' : 'الزوج/ة' ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الوفاة</td><td style="padding:6px 0;"><?= e($case['death_date']) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">شهادة الوفاة</td><td style="padding:6px 0;"><?= e($case['death_certificate_number'] ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الحالة</td><td style="padding:6px 0;font-weight:700;color:<?= match($case['status']) { 'completed' => '#059669', 'fee_paid' => '#2563EB', default => '#D97706' } ?>;"><?= match($case['status']) { 'completed' => 'مكتمل', 'fee_paid' => 'تم الدفع', default => 'مسجّل' } ?></td></tr>
<?php if ($case['transferred_to_member_id']): ?><tr><td style="padding:6px 0;color:#6B7280;">نُقلت إلى</td><td style="padding:6px 0;"><a href="/members/<?= (int) $case['transferred_to_member_id'] ?>" style="color:#0D7377;font-weight:600;">عضو #<?= (int) $case['transferred_to_member_id'] ?></a></td></tr><?php endif; ?>
</table>
<div style="display:flex;justify-content:space-between;align-items:start;">
<table style="width:100%;max-width:600px;font-size:14px;">
<tr><td style="padding:6px 0;color:#6B7280;width:35%;">العضو</td><td style="padding:6px 0;font-weight:600;"><?= e($case['member_name']) ?> (<?= e($case['membership_number'] ?? '—') ?>)</td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">المتوفى</td><td style="padding:6px 0;font-weight:600;"><?= $deceasedLabel ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الوفاة</td><td style="padding:6px 0;"><?= e($case['death_date']) ?></td></tr>
<?php if ($case['death_certificate_number']): ?>
<tr><td style="padding:6px 0;color:#6B7280;">شهادة الوفاة</td><td style="padding:6px 0;"><?= e($case['death_certificate_number']) ?></td></tr>
<?php endif; ?>
<tr><td style="padding:6px 0;color:#6B7280;">الحالة</td><td style="padding:6px 0;font-weight:700;color:<?= $statusColor ?>;"><?= $statusLabel ?></td></tr>
<?php if ($primarySpouse): ?>
<tr><td style="padding:6px 0;color:#6B7280;">الزوجة الأساسية</td><td style="padding:6px 0;font-weight:600;"><?= e($primarySpouse['full_name_ar']) ?></td></tr>
<?php endif; ?>
<?php if ($newMember): ?>
<tr><td style="padding:6px 0;color:#6B7280;">نُقلت العضوية إلى</td><td style="padding:6px 0;"><a href="/members/<?= (int) $newMember['id'] ?>" style="color:#0D7377;font-weight:700;font-size:16px;"><?= e($newMember['full_name_ar']) ?> — رقم <?= e($newMember['membership_number'] ?? '—') ?></a></td></tr>
<?php endif; ?>
</table>
<a href="/death" class="btn btn-outline">← القائمة</a>
</div>
</div>
<?php if (!empty($secondarySpouses)): ?>
<div class="card" style="margin-bottom:20px;padding:20px;">
<h4 style="margin:0 0 10px;color:#6B7280;font-size:14px;">الزوجات الإضافيات (عضويات منفصلة)</h4>
<?php foreach ($secondarySpouses as $ss): ?>
<div style="padding:5px 0;font-size:14px;"><?= e($ss['full_name_ar']) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Fee Breakdown -->
<?php if (bccomp($case['fee_amount'] ?? '0', '0', 2) > 0): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="receipt" style="width:18px;height:18px;color:#D97706;"></i>
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#D97706;font-size:15px;">تفصيل الرسوم</h3>
</div>
<div style="padding:20px;">
<table style="width:100%;max-width:500px;font-size:14px;">
<tr>
<td style="padding:8px 0;color:#6B7280;">رسوم الاستمارة</td>
<td style="padding:8px 0;font-weight:600;direction:ltr;text-align:left;"><?= money($formFee ?? '0') ?></td>
</tr>
<tr>
<td style="padding:8px 0;color:#6B7280;">اشتراك سنوي</td>
<td style="padding:8px 0;font-weight:600;direction:ltr;text-align:left;"><?= money($annualSubBase ?? '0') ?></td>
</tr>
<tr>
<td style="padding:8px 0;color:#6B7280;">رسوم تنمية</td>
<td style="padding:8px 0;font-weight:600;direction:ltr;text-align:left;"><?= money($devFee ?? '0') ?></td>
</tr>
<tr><td style="padding:8px 0;color:#6B7280;">رسوم الاستمارة (570)</td><td style="padding:8px 0;font-weight:600;direction:ltr;text-align:left;"><?= money($fees['formFee']) ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">اشتراك سنوي</td><td style="padding:8px 0;font-weight:600;direction:ltr;text-align:left;"><?= money($fees['annualSubBase']) ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">رسوم تنمية</td><td style="padding:8px 0;font-weight:600;direction:ltr;text-align:left;"><?= money($fees['devFee']) ?></td></tr>
<?php
$secondaryCount = 0;
if (!empty($case['secondary_spouses_json'])) {
$secondaryCount = count(json_decode($case['secondary_spouses_json'], true));
}
if ($secondaryCount > 0): ?>
<tr><td style="padding:8px 0;color:#6B7280;">× <?= $secondaryCount + 1 ?> (أساسية + <?= $secondaryCount ?> إضافية)</td><td style="padding:8px 0;font-weight:600;"></td></tr>
<?php endif; ?>
<tr style="border-top:2px solid #0D7377;">
<td style="padding:12px 0;font-weight:700;font-size:16px;">الإجمالي المطلوب</td>
<td style="padding:12px 0;font-weight:800;font-size:22px;color:#DC2626;direction:ltr;text-align:left;"><?= money($case['fee_amount'] ?? '0') ?></td>
<td style="padding:12px 0;font-weight:800;font-size:22px;color:#DC2626;direction:ltr;text-align:left;"><?= money($case['fee_amount']) ?></td>
</tr>
</table>
</div>
</div>
<?php endif; ?>
<?php if (!in_array($case['status'], ['completed', 'fee_paid']) && bccomp($case['fee_amount'] ?? '0', '0', 2) > 0): ?>
<!-- Action Buttons -->
<?php if ($case['status'] === 'recorded' && bccomp($case['fee_amount'] ?? '0', '0', 2) > 0): ?>
<div class="card" style="padding:20px;margin-bottom:20px;background:#FFF7ED;border:2px solid #F59E0B;">
<h4 style="margin:0 0 15px;color:#D97706;">💰 دفع رسوم نقل العضوية (استمارة + اشتراك سنوي)</h4>
<h4 style="margin:0 0 15px;color:#D97706;">دفع رسوم نقل العضوية</h4>
<form method="POST" action="/death/<?= (int) $case['id'] ?>/pay">
<?= csrf_field() ?>
<input type="hidden" name="amount" value="<?= e($case['fee_amount']) ?>">
<div style="display:flex;gap:10px;align-items:end;">
<div class="form-group"><label class="form-label">المبلغ</label><input type="text" value="<?= money($case['fee_amount']) ?>" class="form-input" style="background:#F3F4F6;font-weight:700;font-size:18px;" readonly></div>
<div class="form-group"><label class="form-label">طريقة الدفع</label><select name="payment_method" class="form-select"><option value="cash">نقدي</option><option value="visa">فيزا</option></select></div>
<button type="submit" class="btn btn-primary" style="padding:10px 25px;" onclick="return confirm('تأكيد الدفع؟')">💰 ادفع</button>
<div class="form-group"><label class="form-label">طريقة الدفع</label><select name="payment_method" class="form-select"><option value="cash">نقدي</option><option value="visa">فيزا</option><option value="bank_transfer">تحويل</option></select></div>
<button type="submit" class="btn btn-primary" style="padding:10px 25px;" onclick="return confirm('تأكيد الدفع؟')">ادفع</button>
</div>
</form>
</div>
<?php endif; ?>
<?php if ($case['status'] !== 'completed'): ?>
<?php if ($case['status'] === 'pending_form_fill'): ?>
<div class="card" style="padding:20px;margin-bottom:20px;background:#F5F3FF;border:2px solid #7C3AED;">
<h4 style="margin:0 0 10px;color:#7C3AED;">ملء استمارة العضوية الجديدة</h4>
<p style="font-size:13px;color:#6B7280;margin-bottom:15px;">يجب ملء استمارة العضوية الكاملة للزوجة قبل إتمام نقل العضوية.</p>
<?php if ($case['primary_spouse_form_filled']): ?>
<p style="color:#059669;font-weight:600;margin-bottom:10px;">✓ تم ملء الاستمارة — يمكن إتمام النقل</p>
<?php else: ?>
<a href="/death/<?= (int) $case['id'] ?>/fill-form" class="btn btn-primary">ملء الاستمارة الآن</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($case['status'] === 'pending_form_fill' && $case['primary_spouse_form_filled']): ?>
<form method="POST" action="/death/<?= (int) $case['id'] ?>/complete">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('سيتم نقل العضوية بنفس الرقم. متأكد؟')">إتمام نقل العضوية</button>
</form>
<?php elseif ($case['status'] === 'fee_paid' && $case['deceased_type'] !== 'primary_member'): ?>
<form method="POST" action="/death/<?= (int) $case['id'] ?>/complete">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('⚠ إتمام حالة الوفاة. متأكد؟')">✅ إتمام الإجراء</button>
<button type="submit" class="btn btn-primary" onclick="return confirm('إتمام حالة الوفاة؟')">إتمام الإجراء</button>
</form>
<?php elseif ($case['status'] === 'recorded' && bccomp($case['fee_amount'] ?? '0', '0', 2) <= 0): ?>
<form method="POST" action="/death/<?= (int) $case['id'] ?>/complete">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('إتمام حالة الوفاة؟')">إتمام الإجراء</button>
</form>
<?php endif; ?>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->endSection(); ?>
......@@ -212,6 +212,12 @@ final class PaymentService
], '`id` = ?', [(int) $payment['receipt_id']]);
}
// Void any linked payment_requests that reference this payment
$db->query(
"UPDATE payment_requests SET status = 'voided', is_voided = 1, updated_at = NOW() WHERE payment_id = ? AND is_voided = 0",
[$paymentId]
);
$db->commit();
EventBus::dispatch('payment.voided', [
......
......@@ -100,24 +100,52 @@ class TransferController extends Controller
return $this->redirect("/transfers/create/{$memberId}")->withError($feeCalc['error'] ?? 'خطأ في حساب الرسوم');
}
// Companion validation: check if new owner brings extra dependents
$targetSpousesCount = (int) $request->post('target_spouses_count', 0);
$targetChildrenCount = (int) $request->post('target_children_count', 0);
$companionSurcharge = '0.00';
$companionBreakdown = null;
$sourceCompanionsCount = null;
$targetCompanionsCount = null;
if ($targetSpousesCount > 0 || $targetChildrenCount > 0) {
$surchargeResult = SeparationFeeCalculator::calculateCompanionSurcharge(
(int) $memberId,
$targetSpousesCount,
$targetChildrenCount
);
$companionSurcharge = $surchargeResult['surcharge'];
$sourceCompanionsCount = $surchargeResult['source_count'];
$targetCompanionsCount = $surchargeResult['target_count'];
if (!empty($surchargeResult['breakdown'])) {
$companionBreakdown = json_encode($surchargeResult['breakdown'], JSON_UNESCAPED_UNICODE);
}
}
$totalWithSurcharge = bcadd($feeCalc['total_fee'], $companionSurcharge, 2);
$employee = App::getInstance()->currentEmployee();
$transferReq = TransferRequest::create([
'source_member_id' => (int) $memberId,
'transfer_type' => $transferType,
'child_id' => $childId,
'spouse_id' => $spouseId,
'source_membership_number'=> $member['membership_number'],
'new_membership_value' => $feeCalc['new_membership_value'],
'years_since_acquisition' => $feeCalc['years_since_acquisition'],
'qualification_code' => $feeCalc['qualification_code'],
'fee_percentage' => $feeCalc['fee_percentage'],
'separation_fee' => $feeCalc['separation_fee'],
'form_fee' => $feeCalc['form_fee'],
'annual_subscription_fee' => $feeCalc['annual_subscription_fee'],
'total_fee' => $feeCalc['total_fee'],
'status' => 'requested',
'notes' => $notes ?: null,
'source_member_id' => (int) $memberId,
'transfer_type' => $transferType,
'child_id' => $childId,
'spouse_id' => $spouseId,
'source_membership_number' => $member['membership_number'],
'new_membership_value' => $feeCalc['new_membership_value'],
'source_companions_count' => $sourceCompanionsCount,
'target_companions_count' => $targetCompanionsCount,
'companion_surcharge' => $companionSurcharge,
'companion_surcharge_breakdown'=> $companionBreakdown,
'years_since_acquisition' => $feeCalc['years_since_acquisition'],
'qualification_code' => $feeCalc['qualification_code'],
'fee_percentage' => $feeCalc['fee_percentage'],
'separation_fee' => $feeCalc['separation_fee'],
'form_fee' => $feeCalc['form_fee'],
'annual_subscription_fee' => $feeCalc['annual_subscription_fee'],
'total_fee' => $totalWithSurcharge,
'status' => 'requested',
'notes' => $notes ?: null,
]);
if (FormBridge::exists('TRANSFER_SEPARATION')) {
......@@ -131,7 +159,7 @@ class TransferController extends Controller
]);
// Send payment to cashier queue
$amount = $feeCalc['total_fee'] ?? '0.00';
$amount = $totalWithSurcharge;
if (bccomp((string) $amount, '0', 2) > 0) {
$result = PaymentRequestService::createRequest([
'member_id' => (int) $memberId,
......@@ -153,7 +181,7 @@ class TransferController extends Controller
);
}
return $this->redirect("/transfers/{$transferReq->id}")->withSuccess('تم تقديم طلب التحويل/الفصل — الإجمالي: ' . money($feeCalc['total_fee']));
return $this->redirect("/transfers/{$transferReq->id}")->withSuccess('تم تقديم طلب التحويل/الفصل — الإجمالي: ' . money($totalWithSurcharge));
}
public function show(Request $request, string $id): Response
......
......@@ -19,6 +19,8 @@ class TransferRequest extends Model
'source_member_id', 'target_member_id', 'transfer_type',
'child_id', 'spouse_id', 'source_membership_number',
'new_membership_number', 'new_membership_value',
'source_companions_count', 'target_companions_count',
'companion_surcharge', 'companion_surcharge_breakdown',
'years_since_acquisition', 'qualification_code',
'fee_percentage', 'separation_fee', 'form_fee',
'annual_subscription_fee', 'total_fee',
......
......@@ -121,4 +121,78 @@ final class SeparationFeeCalculator
$data = RuleEngine::require($ruleCode);
return $data['percentage'];
}
/**
* Calculate surcharge when new owner has more companions than the original member.
* Extra spouses/children are charged standard addition fees.
*/
public static function calculateCompanionSurcharge(
int $sourceMemberId,
int $targetSpousesCount,
int $targetChildrenCount
): array {
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [$sourceMemberId]);
if (!$member) {
return ['surcharge' => '0.00', 'breakdown' => [], 'source_count' => 0, 'target_count' => 0];
}
$sourceSpouses = (int) $db->selectOne(
"SELECT COUNT(*) as cnt FROM spouses WHERE member_id = ? AND is_archived = 0 AND status = 'active'",
[$sourceMemberId]
)['cnt'];
$sourceChildren = (int) $db->selectOne(
"SELECT COUNT(*) as cnt FROM children WHERE member_id = ? AND is_archived = 0 AND status = 'active'",
[$sourceMemberId]
)['cnt'];
$sourceTotal = $sourceSpouses + $sourceChildren;
$targetTotal = $targetSpousesCount + $targetChildrenCount;
$breakdown = [];
$surcharge = '0.00';
$membershipValue = $member['membership_value'] ?? '0.00';
// Extra spouses beyond what source had
$extraSpouses = max(0, $targetSpousesCount - $sourceSpouses);
if ($extraSpouses > 0) {
$spouseBasePct = RuleEngine::getValue('SPOUSE_BASE_MEMBER_FEE', 'percentage') ?? '30.00';
$perSpouseFee = bcdiv(bcmul($membershipValue, $spouseBasePct, 4), '100', 2);
$spouseSurcharge = bcmul($perSpouseFee, (string) $extraSpouses, 2);
$surcharge = bcadd($surcharge, $spouseSurcharge, 2);
$breakdown[] = [
'type' => 'extra_spouses',
'count' => $extraSpouses,
'per_unit' => $perSpouseFee,
'percentage' => $spouseBasePct,
'total' => $spouseSurcharge,
];
}
// Extra children beyond what source had
$extraChildren = max(0, $targetChildrenCount - $sourceChildren);
if ($extraChildren > 0) {
$childBasePct = RuleEngine::getValue('CHILD_ADDITION_FEE', 'percentage') ?? '10.00';
$perChildFee = bcdiv(bcmul($membershipValue, $childBasePct, 4), '100', 2);
$childSurcharge = bcmul($perChildFee, (string) $extraChildren, 2);
$surcharge = bcadd($surcharge, $childSurcharge, 2);
$breakdown[] = [
'type' => 'extra_children',
'count' => $extraChildren,
'per_unit' => $perChildFee,
'percentage' => $childBasePct,
'total' => $childSurcharge,
];
}
return [
'surcharge' => $surcharge,
'breakdown' => $breakdown,
'source_count' => $sourceTotal,
'target_count' => $targetTotal,
'extra_spouses' => $extraSpouses ?? 0,
'extra_children' => $extraChildren ?? 0,
];
}
}
\ No newline at end of file
......@@ -94,10 +94,22 @@ final class TransferProcessor
'created_by' => $employee ? (int) $employee->id : null,
];
// Transfer the SAME membership number to the new member
$sameNumber = $sourceMember['membership_number'];
$newMemberData['membership_number'] = $sameNumber;
$newMemberId = $db->insert('members', $newMemberData);
// 4. Assign new membership number
$newNumber = MemberNumberGenerator::assign($newMemberId);
// 4. Clear membership number from source and archive
$db->update('members', [
'membership_number' => null,
'status' => 'transferred',
'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $sourceMember['id']]);
$newNumber = $sameNumber;
// 5. Record number chain
ArchiveService::recordNumberTransfer(
......@@ -108,7 +120,7 @@ final class TransferProcessor
null
);
// 6. Update source records
// 6. Update source dependents — mark as separated if it's a child/spouse separation
if ($request['child_id']) {
$db->update('children', [
'status' => 'separated',
......@@ -122,6 +134,13 @@ final class TransferProcessor
], '`id` = ?', [(int) $request['spouse_id']]);
}
// Transfer all dependents from source to new member (full membership transfer)
if (!$request['child_id'] && !$request['spouse_id']) {
$db->query("UPDATE spouses SET member_id = ?, updated_at = NOW() WHERE member_id = ? AND is_archived = 0", [$newMemberId, (int) $sourceMember['id']]);
$db->query("UPDATE children SET member_id = ?, updated_at = NOW() WHERE member_id = ? AND is_archived = 0", [$newMemberId, (int) $sourceMember['id']]);
$db->query("UPDATE temporary_members SET member_id = ?, updated_at = NOW() WHERE member_id = ? AND is_archived = 0", [$newMemberId, (int) $sourceMember['id']]);
}
// 7. Mark transfer request complete
$db->update('transfer_requests', [
'target_member_id' => $newMemberId,
......
<?php
declare(strict_types=1);
return function (\App\Core\Database $db): void {
$table = 'death_cases';
$columnsToAdd = [
'primary_spouse_id' => "BIGINT UNSIGNED NULL AFTER `spouse_id`",
'secondary_spouses_json' => "TEXT NULL AFTER `primary_spouse_id`",
'primary_spouse_form_filled' => "TINYINT(1) NOT NULL DEFAULT 0 AFTER `secondary_spouses_json`",
'child_id' => "BIGINT UNSIGNED NULL AFTER `primary_spouse_form_filled`",
];
foreach ($columnsToAdd as $col => $definition) {
$exists = $db->selectOne(
"SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?",
[$table, $col]
);
if (!$exists) {
$db->raw("ALTER TABLE `{$table}` ADD COLUMN `{$col}` {$definition}");
}
}
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$db->raw("
ALTER TABLE transfer_requests
ADD COLUMN source_companions_count INT UNSIGNED NULL AFTER new_membership_value,
ADD COLUMN target_companions_count INT UNSIGNED NULL AFTER source_companions_count,
ADD COLUMN companion_surcharge DECIMAL(15,2) NOT NULL DEFAULT 0.00 AFTER target_companions_count,
ADD COLUMN companion_surcharge_breakdown TEXT NULL AFTER companion_surcharge
");
};
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