Commit b1d4c95d authored by Administrator's avatar Administrator

Update 37 files via Son of Anton

parent a7292977
<?php
declare(strict_types=1);
namespace App\Modules\Death\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Death\Models\DeathCase;
use App\Modules\Archive\Services\ArchiveService;
use App\Modules\Rules\Services\RuleEngine;
class DeathController extends Controller
{
public function index(Request $request): Response
{
$filters = ['search' => trim((string) $request->get('q', '')), 'status' => $request->get('status', '')];
$page = max(1, (int) $request->get('page', 1));
$result = DeathCase::search($filters, 25, $page);
return $this->view('Death.Views.index', ['rows' => $result['data'], 'pagination' => $result['pagination'], 'filters' => $filters]);
}
public function create(Request $request, string $memberId): Response
{
$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]);
return $this->view('Death.Views.create', ['member' => $member, 'spouses' => $spouses]);
}
public function store(Request $request, string $memberId): Response
{
$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('العضو غير موجود');
$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', ''));
if (!$deceasedType || !$deathDate) {
return $this->redirect("/death/create/{$memberId}")->withError('بيانات الوفاة غير مكتملة');
}
// Calculate fee (form fee + annual renewal)
$formFeeData = RuleEngine::get('FORM_TRANSFER_FEE');
$formFee = $formFeeData['amount'] ?? '570.00';
$annualSub = '527.00'; // 492 + 35 development
$totalFee = bcadd($formFee, $annualSub, 2);
$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,
]);
EventBus::dispatch('death.recorded', ['case_id' => (int) $case->id, 'member_id' => (int) $memberId, 'type' => $deceasedType]);
return $this->redirect("/death/{$case->id}")->withSuccess('تم تسجيل حالة الوفاة');
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$case = $db->selectOne(
"SELECT dc.*, m.full_name_ar as member_name, m.membership_number
FROM death_cases dc JOIN members m ON m.id = dc.member_id WHERE dc.id = ?",
[(int) $id]
);
if (!$case) return $this->redirect('/death')->withError('الحالة غير موجودة');
return $this->view('Death.Views.show', ['case' => $case]);
}
public function complete(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$case = $db->selectOne("SELECT * FROM death_cases WHERE id = ?", [(int) $id]);
if (!$case || $case['status'] === 'completed') return $this->redirect('/death')->withError('الحالة غير صالحة');
$employee = App::getInstance()->currentEmployee();
$db->beginTransaction();
try {
$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'] === 'primary_member') {
// Primary member death: transfer to spouse with SAME number
$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
$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);
$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('تم نقل العضوية للزوج/ة بنفس رقم العضوية');
}
}
$db->update('death_cases', [
'archive_snapshot_id' => $snapshotId,
'status' => 'completed',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
$db->commit();
return $this->redirect("/death/{$id}")->withSuccess('تم إتمام حالة الوفاة');
} catch (\Throwable $e) {
$db->rollBack();
return $this->redirect("/death/{$id}")->withError('فشل: ' . $e->getMessage());
}
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Death\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class DeathCase extends Model
{
protected static string $table = 'death_cases';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $dispatchEvents = true;
protected static array $fillable = [
'member_id', 'deceased_type', 'death_date',
'death_certificate_number', 'death_certificate_path',
'spouse_id', 'child_id', 'transferred_to_member_id',
'same_membership_number', 'fee_amount',
'archive_snapshot_id', 'workflow_instance_id', 'status', 'notes',
];
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['status'])) { $where .= ' AND dc.status = ?'; $params[] = $filters['status']; }
if (!empty($filters['search'])) {
$where .= ' AND (m.full_name_ar LIKE ? OR m.membership_number LIKE ?)';
$s = '%' . $filters['search'] . '%'; $params[] = $s; $params[] = $s;
}
$countRow = $db->selectOne("SELECT COUNT(*) as cnt FROM death_cases dc JOIN members m ON m.id = dc.member_id WHERE {$where}", $params);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT dc.*, m.full_name_ar as member_name, m.membership_number
FROM death_cases dc JOIN members m ON m.id = dc.member_id
WHERE {$where} ORDER BY dc.created_at DESC LIMIT {$perPage} OFFSET {$offset}", $params
);
return ['data' => $rows, 'pagination' => Pagination::paginate($total, $perPage, $page)];
}
}
\ No newline at end of file
<?php
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'], 'transfer.initiate'],
['GET', '/death/{id}', 'Death\Controllers\DeathController@show', ['auth'], 'transfer.view'],
['POST', '/death/{id}/complete', 'Death\Controllers\DeathController@complete', ['auth'], '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'); ?>
<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="form-group">
<label class="form-label">المتوفى <span style="color:#DC2626;">*</span></label>
<select name="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; ?>
</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>
<?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>
</div>
<button type="submit" class="btn btn-primary">تسجيل حالة الوفاة</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حالات الوفاة<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card"><div class="table-responsive"><table class="data-table"><thead><tr><th>#</th><th>العضو</th><th>النوع</th><th>تاريخ الوفاة</th><th>الرسوم</th><th>الحالة</th><th>الإجراءات</th></tr></thead><tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= (int) $r['id'] ?></td>
<td><a href="/members/<?= (int) $r['member_id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($r['member_name']) ?></a></td>
<td><?= $r['deceased_type'] === 'primary_member' ? 'العضو الرئيسي' : ($r['deceased_type'] === 'spouse' ? 'الزوج/ة' : $r['deceased_type']) ?></td>
<td style="font-size:13px;"><?= e($r['death_date']) ?></td>
<td style="font-weight:600;"><?= money($r['fee_amount'] ?? '0') ?></td>
<td><span style="color:<?= $r['status'] === 'completed' ? '#059669' : '#D97706' ?>;font-weight:600;"><?= $r['status'] === 'completed' ? 'مكتمل' : 'مسجّل' ?></span></td>
<td><a href="/death/<?= (int) $r['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?><tr><td colspan="7" style="text-align:center;padding:40px;color:#6B7280;">لا توجد حالات وفاة</td></tr><?php endif; ?>
</tbody></table></div></div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حالة وفاة #<?= (int) $case['id'] ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="padding: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;"><?= $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;"><?= $case['same_membership_number'] ? 'نعم' : 'لا' ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الرسوم</td><td style="padding:6px 0;font-weight:700;color:#0D7377;"><?= money($case['fee_amount'] ?? '0') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الحالة</td><td style="padding:6px 0;font-weight:700;color:<?= $case['status'] === 'completed' ? '#059669' : '#D97706' ?>;"><?= $case['status'] === 'completed' ? 'مكتمل' : 'مسجّل' ?></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>
<?php if ($case['status'] !== 'completed'): ?>
<form method="POST" action="/death/<?= (int) $case['id'] ?>/complete" style="margin-top:20px;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('⚠ إتمام حالة الوفاة. متأكد؟')">إتمام الإجراء</button>
</form>
<?php endif; ?>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
// Death module bootstraps under Transfers menu (already registered)
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Divorce\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Divorce\Models\DivorceCase;
use App\Modules\Divorce\Services\DivorceFeeCalculator;
use App\Modules\Archive\Services\ArchiveService;
use App\Modules\Members\Services\MemberNumberGenerator;
class DivorceController extends Controller
{
public function index(Request $request): Response
{
$filters = ['search' => trim((string) $request->get('q', '')), 'status' => $request->get('status', '')];
$page = max(1, (int) $request->get('page', 1));
$result = DivorceCase::search($filters, 25, $page);
return $this->view('Divorce.Views.index', ['rows' => $result['data'], 'pagination' => $result['pagination'], 'filters' => $filters]);
}
public function create(Request $request, string $memberId): Response
{
$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]);
if (empty($spouses)) return $this->redirect("/members/{$memberId}")->withError('لا يوجد زوج/زوجة مسجلة');
return $this->view('Divorce.Views.create', ['member' => $member, 'spouses' => $spouses]);
}
public function store(Request $request, string $memberId): Response
{
$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('العضو غير موجود');
$spouseId = (int) $request->post('spouse_id', 0);
$divorceDate = trim($request->post('divorce_date', ''));
$notes = trim($request->post('notes', ''));
if (!$spouseId || !$divorceDate) {
return $this->redirect("/divorce/create/{$memberId}")->withError('بيانات الطلاق غير مكتملة');
}
$feeCalc = DivorceFeeCalculator::calculate((int) $memberId, $spouseId, $divorceDate);
if (!($feeCalc['success'] ?? false)) {
return $this->redirect("/divorce/create/{$memberId}")->withError($feeCalc['error'] ?? 'خطأ');
}
$case = DivorceCase::create([
'member_id' => (int) $memberId,
'spouse_id' => $spouseId,
'divorce_date' => $divorceDate,
'divorce_case_type' => $feeCalc['case_type'],
'request_date' => date('Y-m-d'),
'membership_acquisition_date' => substr($member['created_at'] ?? '', 0, 10),
'years_of_membership' => $feeCalc['years_of_membership'],
'has_children_on_membership' => $feeCalc['has_children'] ? 1 : 0,
'fee_percentage' => $feeCalc['fee_percentage'],
'fee_amount' => $feeCalc['total_fee'],
'status' => 'submitted',
'notes' => $notes ?: null,
]);
EventBus::dispatch('divorce.submitted', ['case_id' => (int) $case->id, 'member_id' => (int) $memberId]);
return $this->redirect("/divorce/{$case->id}")->withSuccess('تم تسجيل حالة الطلاق — النوع: ' . DivorceCase::getCaseTypeLabel($feeCalc['case_type']) . ' — الرسوم: ' . money($feeCalc['total_fee']));
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$case = $db->selectOne(
"SELECT dc.*, m.full_name_ar as member_name, m.membership_number, s.full_name_ar as spouse_name
FROM divorce_cases dc JOIN members m ON m.id = dc.member_id JOIN spouses s ON s.id = dc.spouse_id WHERE dc.id = ?",
[(int) $id]
);
if (!$case) return $this->redirect('/divorce')->withError('الحالة غير موجودة');
return $this->view('Divorce.Views.show', ['case' => $case]);
}
public function complete(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$case = $db->selectOne("SELECT * FROM divorce_cases WHERE id = ?", [(int) $id]);
if (!$case || $case['status'] === 'completed') return $this->redirect('/divorce')->withError('الحالة غير صالحة');
$employee = App::getInstance()->currentEmployee();
$db->beginTransaction();
try {
// Archive snapshot
$snapshotId = ArchiveService::takeSnapshot('members', (int) $case['member_id'], 'divorce', 'طلاق — حالة #' . $id);
// Create new member for spouse
$spouse = $db->selectOne("SELECT * FROM spouses WHERE id = ?", [(int) $case['spouse_id']]);
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $case['member_id']]);
$newMemberId = $db->insert('members', [
'full_name_ar' => $spouse['full_name_ar'],
'full_name_en' => $spouse['full_name_en'] ?? null,
'national_id' => $spouse['national_id'],
'date_of_birth' => $spouse['date_of_birth'],
'age_years' => $spouse['age_years'],
'age_months' => $spouse['age_months'],
'gender' => $spouse['gender'],
'nationality' => $spouse['nationality'] ?? 'مصري',
'branch_id' => (int) $member['branch_id'],
'membership_type' => 'working',
'member_category' => 'working_member',
'status' => 'active',
'qualification_id' => $spouse['qualification_id'] ?? $member['qualification_id'],
'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,
]);
$newNumber = MemberNumberGenerator::assign($newMemberId);
// Archive spouse
$db->update('spouses', ['status' => 'divorced', 'is_archived' => 1, 'archived_at' => date('Y-m-d H:i:s')], '`id` = ?', [(int) $case['spouse_id']]);
// Update case
$db->update('divorce_cases', [
'spouse_new_member_id' => $newMemberId,
'spouse_new_membership_number' => $newNumber,
'archive_snapshot_id' => $snapshotId,
'status' => 'completed',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
ArchiveService::recordNumberTransfer($newNumber, 'divorce', 'members', $newMemberId);
$db->commit();
EventBus::dispatch('divorce.completed', ['case_id' => (int) $id, 'new_member_id' => $newMemberId, 'new_number' => $newNumber]);
return $this->redirect("/divorce/{$id}")->withSuccess('تم إتمام حالة الطلاق — رقم العضوية الجديد: ' . $newNumber);
} catch (\Throwable $e) {
$db->rollBack();
return $this->redirect("/divorce/{$id}")->withError('فشل: ' . $e->getMessage());
}
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Divorce\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class DivorceCase extends Model
{
protected static string $table = 'divorce_cases';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $dispatchEvents = true;
protected static array $fillable = [
'member_id', 'spouse_id', 'divorce_date', 'divorce_case_type',
'request_date', 'membership_acquisition_date', 'years_of_membership',
'has_children_on_membership', 'fee_percentage', 'fee_amount',
'spouse_new_member_id', 'spouse_new_membership_number',
'children_assignment_json', 'archive_snapshot_id',
'workflow_instance_id', 'status', 'notes',
];
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['status'])) { $where .= ' AND dc.status = ?'; $params[] = $filters['status']; }
if (!empty($filters['search'])) {
$where .= ' AND (m.full_name_ar LIKE ? OR m.membership_number LIKE ?)';
$s = '%' . $filters['search'] . '%'; $params[] = $s; $params[] = $s;
}
$countRow = $db->selectOne("SELECT COUNT(*) as cnt FROM divorce_cases dc JOIN members m ON m.id = dc.member_id WHERE {$where}", $params);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT dc.*, m.full_name_ar as member_name, m.membership_number,
s.full_name_ar as spouse_name
FROM divorce_cases dc
JOIN members m ON m.id = dc.member_id
JOIN spouses s ON s.id = dc.spouse_id
WHERE {$where} ORDER BY dc.created_at DESC LIMIT {$perPage} OFFSET {$offset}", $params
);
return ['data' => $rows, 'pagination' => Pagination::paginate($total, $perPage, $page)];
}
public static function getCaseTypeLabel(string $type): string
{
return match ($type) {
'both_working' => 'كلاهما عامل قبل الزواج',
'same_form' => 'قُبلا في استمارة واحدة',
'joined_after' => 'الزوج/ة انضم بعد العضوية',
default => $type,
};
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/divorce', 'Divorce\Controllers\DivorceController@index', ['auth'], 'transfer.view'],
['GET', '/divorce/create/{memberId}', 'Divorce\Controllers\DivorceController@create', ['auth'], 'transfer.initiate'],
['POST', '/divorce/store/{memberId}', 'Divorce\Controllers\DivorceController@store', ['auth'], 'transfer.initiate'],
['GET', '/divorce/{id}', 'Divorce\Controllers\DivorceController@show', ['auth'], 'transfer.view'],
['POST', '/divorce/{id}/complete', 'Divorce\Controllers\DivorceController@complete',['auth'], 'transfer.approve'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Divorce\Services;
use App\Core\App;
use App\Modules\Rules\Services\RuleEngine;
final class DivorceFeeCalculator
{
/**
* Determine divorce case type and calculate fee.
*/
public static function calculate(int $memberId, int $spouseId, string $divorceDate): array
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [$memberId]);
$spouse = $db->selectOne("SELECT * FROM spouses WHERE id = ?", [$spouseId]);
if (!$member || !$spouse) {
return ['success' => false, 'error' => 'بيانات العضو أو الزوج غير موجودة'];
}
// Check 1-year window from divorce date
$divorceTs = strtotime($divorceDate);
$windowData = RuleEngine::get('DIVORCE_REQUEST_WINDOW');
$maxYears = $windowData['max_years'] ?? 1;
$deadline = strtotime("+{$maxYears} years", $divorceTs);
if (time() > $deadline) {
return ['success' => false, 'error' => "انتهت مهلة تقديم الطلب ({$maxYears} سنة من تاريخ الطلاق)"];
}
// Determine case type
$memberCreated = $member['created_at'] ?? $member['form_date'];
$spouseJoinDate = $spouse['join_date'] ?? $spouse['created_at'];
$membershipValue = $member['membership_value'] ?? '0.00';
// Check children
$childCount = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM children WHERE member_id = ? AND is_archived = 0",
[$memberId]
)['cnt'] ?? 0);
$hasChildren = $childCount > 0;
// Check 5-year minimum (waived if children)
$minYearsData = RuleEngine::get('DIVORCE_MIN_MEMBERSHIP_YEARS');
$minYears = $minYearsData['min_years'] ?? 5;
$waivedIfChildren = $minYearsData['waived_if_children'] ?? true;
$yearsMembership = (int) ((time() - strtotime($memberCreated)) / (365.25 * 86400));
if ($yearsMembership < $minYears && !($waivedIfChildren && $hasChildren)) {
return ['success' => false, 'error' => "الحد الأدنى لسنوات العضوية {$minYears} سنوات (يُعفى في حالة وجود أبناء)"];
}
// Determine case type and fee
$caseType = 'joined_after'; // default: most common
$feePercentage = '50.00';
$feeType = 'acquired_member';
// Case 1: Both working before marriage
// Simplified detection: if spouse join_date == member creation date (same form)
$memberCreatedDate = substr($memberCreated, 0, 10);
$spouseJoinDateStr = substr($spouseJoinDate, 0, 10);
if ($memberCreatedDate === $spouseJoinDateStr) {
// Could be Case 2: same form
$sameFormData = RuleEngine::get('DIVORCE_SAME_FORM_FEE');
$caseType = 'same_form';
$feePercentage = $sameFormData['percentage'] ?? '10.00';
$feeType = $sameFormData['treat_as'] ?? 'membership_basis';
} else {
// Case 3: joined after
$joinedAfterData = RuleEngine::get('DIVORCE_JOINED_AFTER_FEE');
$caseType = 'joined_after';
$feePercentage = $joinedAfterData['percentage'] ?? '50.00';
$feeType = $joinedAfterData['treat_as'] ?? 'acquired_member';
}
$feeAmount = bcdiv(bcmul($membershipValue, $feePercentage, 4), '100', 2);
// Form fee
$formFeeData = RuleEngine::get('FORM_TRANSFER_FEE');
$formFee = $formFeeData['amount'] ?? '570.00';
$totalFee = bcadd($feeAmount, $formFee, 2);
return [
'success' => true,
'case_type' => $caseType,
'fee_type' => $feeType,
'fee_percentage' => $feePercentage,
'fee_amount' => $feeAmount,
'form_fee' => $formFee,
'total_fee' => $totalFee,
'membership_value' => $membershipValue,
'years_of_membership' => $yearsMembership,
'has_children' => $hasChildren,
'child_count' => $childCount,
];
}
}
\ 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'); ?>
<form method="POST" action="/divorce/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="form-group">
<label class="form-label">الزوج/الزوجة <span style="color:#DC2626;">*</span></label>
<select name="spouse_id" class="form-select" required>
<option value="">-- اختر --</option>
<?php foreach ($spouses as $s): ?><option value="<?= (int) $s['id'] ?>"><?= e($s['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="divorce_date" class="form-input" required max="<?= e(date('Y-m-d')) ?>">
</div>
<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>
</div>
<button type="submit" class="btn btn-primary">تسجيل حالة الطلاق</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حالات الطلاق<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card"><div class="table-responsive"><table class="data-table"><thead><tr><th>#</th><th>العضو</th><th>الزوج/ة</th><th>تاريخ الطلاق</th><th>النوع</th><th>الرسوم</th><th>الحالة</th><th>الإجراءات</th></tr></thead><tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= (int) $r['id'] ?></td>
<td><a href="/members/<?= (int) $r['member_id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($r['member_name']) ?></a></td>
<td><?= e($r['spouse_name']) ?></td>
<td style="font-size:13px;"><?= e($r['divorce_date']) ?></td>
<td style="font-size:13px;"><?= e(\App\Modules\Divorce\Models\DivorceCase::getCaseTypeLabel($r['divorce_case_type'])) ?></td>
<td style="font-weight:600;"><?= money($r['fee_amount'] ?? '0') ?></td>
<td><span style="color:<?= $r['status'] === 'completed' ? '#059669' : '#D97706' ?>;font-weight:600;"><?= $r['status'] === 'completed' ? 'مكتمل' : ($r['status'] === 'submitted' ? 'مقدّم' : $r['status']) ?></span></td>
<td><a href="/divorce/<?= (int) $r['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?><tr><td colspan="8" style="text-align:center;padding:40px;color:#6B7280;">لا توجد حالات طلاق</td></tr><?php endif; ?>
</tbody></table></div></div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حالة طلاق #<?= (int) $case['id'] ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="padding: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;"><a href="/members/<?= (int) $case['member_id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($case['member_name']) ?></a></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الزوج/ة</td><td style="padding:6px 0;"><?= e($case['spouse_name']) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الطلاق</td><td style="padding:6px 0;"><?= e($case['divorce_date']) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">نوع الحالة</td><td style="padding:6px 0;font-weight:600;"><?= e(\App\Modules\Divorce\Models\DivorceCase::getCaseTypeLabel($case['divorce_case_type'])) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">النسبة</td><td style="padding:6px 0;"><?= e($case['fee_percentage'] ?? '0') ?>%</td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الرسوم</td><td style="padding:6px 0;font-weight:700;color:#0D7377;"><?= money($case['fee_amount'] ?? '0') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الحالة</td><td style="padding:6px 0;font-weight:700;color:<?= $case['status'] === 'completed' ? '#059669' : '#D97706' ?>;"><?= $case['status'] === 'completed' ? 'مكتمل' : 'مقدّم' ?></td></tr>
<?php if ($case['spouse_new_membership_number']): ?><tr><td style="padding:6px 0;color:#6B7280;">رقم العضوية الجديد</td><td style="padding:6px 0;font-weight:700;color:#0D7377;font-size:18px;"><?= e($case['spouse_new_membership_number']) ?></td></tr><?php endif; ?>
</table>
<?php if ($case['status'] !== 'completed'): ?>
<form method="POST" action="/divorce/<?= (int) $case['id'] ?>/complete" style="margin-top:20px;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('⚠ إتمام حالة الطلاق — سيتم إنشاء عضوية جديدة. متأكد؟')">إتمام حالة الطلاق</button>
</form>
<?php endif; ?>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
// Divorce module bootstraps under Transfers menu (already registered)
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Transfers\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Transfers\Models\TransferRequest;
use App\Modules\Transfers\Services\SeparationFeeCalculator;
use App\Modules\Transfers\Services\TransferProcessor;
class TransferController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'search' => trim((string) $request->get('q', '')),
'status' => $request->get('status', ''),
'transfer_type' => $request->get('transfer_type', ''),
];
$page = max(1, (int) $request->get('page', 1));
$result = TransferRequest::search($filters, 25, $page);
return $this->view('Transfers.Views.index', [
'rows' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
]);
}
public function create(Request $request, string $memberId): Response
{
$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('العضو غير موجود');
$children = $db->select("SELECT * FROM children WHERE member_id = ? AND is_archived = 0 AND status = 'active'", [(int) $memberId]);
$spouses = $db->select("SELECT * FROM spouses WHERE member_id = ? AND is_archived = 0 AND status = 'active'", [(int) $memberId]);
return $this->view('Transfers.Views.create', [
'member' => $member,
'children' => $children,
'spouses' => $spouses,
]);
}
public function store(Request $request, string $memberId): Response
{
$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('العضو غير موجود');
$transferType = trim($request->post('transfer_type', ''));
$childId = $request->post('child_id') ? (int) $request->post('child_id') : null;
$spouseId = $request->post('spouse_id') ? (int) $request->post('spouse_id') : null;
$notes = trim($request->post('notes', ''));
$validTypes = ['child_separation', 'child_mandatory_25', 'sports_conversion', 'cross_branch'];
if (!in_array($transferType, $validTypes)) {
return $this->redirect("/transfers/create/{$memberId}")->withError('نوع التحويل غير صالح');
}
if (in_array($transferType, ['child_separation', 'child_mandatory_25']) && !$childId) {
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب اختيار الابن/الابنة');
}
// Calculate fees
$qualCode = null;
if ($childId) {
$child = $db->selectOne("SELECT * FROM children WHERE id = ?", [$childId]);
// Use child's qualification if available, otherwise parent's
}
$feeCalc = SeparationFeeCalculator::calculate((int) $memberId, $childId, $qualCode);
if (!($feeCalc['success'] ?? false)) {
return $this->redirect("/transfers/create/{$memberId}")->withError($feeCalc['error'] ?? 'خطأ في حساب الرسوم');
}
$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,
]);
EventBus::dispatch('transfer.requested', [
'transfer_id' => (int) $transferReq->id,
'member_id' => (int) $memberId,
'type' => $transferType,
]);
return $this->redirect("/transfers/{$transferReq->id}")->withSuccess('تم تقديم طلب التحويل/الفصل — الإجمالي: ' . money($feeCalc['total_fee']));
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$transfer = $db->selectOne(
"SELECT tr.*, m.full_name_ar as source_name, m.membership_number as source_number,
tm.full_name_ar as target_name, tm.membership_number as target_number,
c.full_name_ar as child_name, s.full_name_ar as spouse_name,
e.full_name_ar as approved_by_name
FROM transfer_requests tr
JOIN members m ON m.id = tr.source_member_id
LEFT JOIN members tm ON tm.id = tr.target_member_id
LEFT JOIN children c ON c.id = tr.child_id
LEFT JOIN spouses s ON s.id = tr.spouse_id
LEFT JOIN employees e ON e.id = tr.approved_by
WHERE tr.id = ?",
[(int) $id]
);
if (!$transfer) return $this->redirect('/transfers')->withError('طلب التحويل غير موجود');
return $this->view('Transfers.Views.show', ['transfer' => $transfer]);
}
public function approve(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$transfer = $db->selectOne("SELECT * FROM transfer_requests WHERE id = ? AND status = 'requested'", [(int) $id]);
if (!$transfer) return $this->redirect('/transfers')->withError('الطلب غير موجود أو تم التعامل معه');
$employee = App::getInstance()->currentEmployee();
$boardNotes = trim($request->post('board_decision_notes', ''));
$db->update('transfer_requests', [
'status' => 'approved',
'approved_by' => $employee ? (int) $employee->id : null,
'approved_at' => date('Y-m-d H:i:s'),
'board_decision_notes' => $boardNotes ?: null,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
EventBus::dispatch('transfer.approved', ['transfer_id' => (int) $id]);
return $this->redirect("/transfers/{$id}")->withSuccess('تمت الموافقة على طلب التحويل');
}
public function reject(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$transfer = $db->selectOne("SELECT * FROM transfer_requests WHERE id = ? AND status = 'requested'", [(int) $id]);
if (!$transfer) return $this->redirect('/transfers')->withError('الطلب غير موجود');
$reason = trim($request->post('reject_reason', ''));
$employee = App::getInstance()->currentEmployee();
$db->update('transfer_requests', [
'status' => 'rejected',
'board_decision_notes' => $reason ?: 'مرفوض',
'approved_by' => $employee ? (int) $employee->id : null,
'approved_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
return $this->redirect('/transfers')->withSuccess('تم رفض طلب التحويل');
}
public function complete(Request $request, string $id): Response
{
$result = TransferProcessor::execute((int) $id);
if (!$result['success']) {
return $this->redirect("/transfers/{$id}")->withError($result['error']);
}
return $this->redirect("/transfers/{$id}")->withSuccess(
'تم إتمام التحويل — رقم العضوية الجديد: ' . $result['new_number']
);
}
public function preview(Request $request, string $memberId): Response
{
$feeCalc = SeparationFeeCalculator::calculate((int) $memberId);
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $memberId]);
return $this->view('Transfers.Views.preview', [
'member' => $member,
'feeCalc' => $feeCalc,
]);
}
public function calculateFee(Request $request): Response
{
$memberId = (int) $request->post('member_id', 0);
$childId = $request->post('child_id') ? (int) $request->post('child_id') : null;
$qualCode = $request->post('qualification_code') ?: null;
$result = SeparationFeeCalculator::calculate($memberId, $childId, $qualCode);
return $this->json($result);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Transfers\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class TransferRequest extends Model
{
protected static string $table = 'transfer_requests';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $dispatchEvents = true;
protected static array $fillable = [
'source_member_id', 'target_member_id', 'transfer_type',
'child_id', 'spouse_id', 'source_membership_number',
'new_membership_number', 'new_membership_value',
'years_since_acquisition', 'qualification_code',
'fee_percentage', 'separation_fee', 'form_fee',
'annual_subscription_fee', 'total_fee',
'archive_snapshot_id', 'workflow_instance_id',
'board_decision_notes', 'approved_by', 'approved_at',
'completed_at', 'status', 'notes',
];
public static function getForMember(int $memberId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT tr.*, m.full_name_ar as source_name, m.membership_number as source_number,
tm.full_name_ar as target_name, tm.membership_number as target_number
FROM transfer_requests tr
JOIN members m ON m.id = tr.source_member_id
LEFT JOIN members tm ON tm.id = tr.target_member_id
WHERE tr.source_member_id = ? OR tr.target_member_id = ?
ORDER BY tr.created_at DESC",
[$memberId, $memberId]
);
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['status'])) {
$where .= ' AND tr.status = ?';
$params[] = $filters['status'];
}
if (!empty($filters['transfer_type'])) {
$where .= ' AND tr.transfer_type = ?';
$params[] = $filters['transfer_type'];
}
if (!empty($filters['search'])) {
$where .= ' AND (m.full_name_ar LIKE ? OR m.membership_number LIKE ?)';
$s = '%' . $filters['search'] . '%';
$params[] = $s;
$params[] = $s;
}
$countRow = $db->selectOne("SELECT COUNT(*) as cnt FROM transfer_requests tr JOIN members m ON m.id = tr.source_member_id WHERE {$where}", $params);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT tr.*, m.full_name_ar as source_name, m.membership_number as source_number,
tm.full_name_ar as target_name, c.full_name_ar as child_name, s.full_name_ar as spouse_name
FROM transfer_requests tr
JOIN members m ON m.id = tr.source_member_id
LEFT JOIN members tm ON tm.id = tr.target_member_id
LEFT JOIN children c ON c.id = tr.child_id
LEFT JOIN spouses s ON s.id = tr.spouse_id
WHERE {$where} ORDER BY tr.created_at DESC LIMIT {$perPage} OFFSET {$offset}",
$params
);
return ['data' => $rows, 'pagination' => Pagination::paginate($total, $perPage, $page)];
}
public static function getTransferTypeLabel(string $type): string
{
return match ($type) {
'child_separation' => 'فصل أبناء',
'child_mandatory_25' => 'تحويل وجوبي (25 سنة)',
'divorce' => 'طلاق',
'death' => 'وفاة',
'waiver' => 'تنازل',
'sports_conversion' => 'تحويل رياضي',
'cross_branch' => 'تحويل بين فروع',
default => $type,
};
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/transfers', 'Transfers\Controllers\TransferController@index', ['auth'], 'transfer.view'],
['GET', '/transfers/create/{memberId}', 'Transfers\Controllers\TransferController@create', ['auth'], 'transfer.initiate'],
['POST', '/transfers/store/{memberId}', 'Transfers\Controllers\TransferController@store', ['auth'], 'transfer.initiate'],
['GET', '/transfers/{id}', 'Transfers\Controllers\TransferController@show', ['auth'], 'transfer.view'],
['POST', '/transfers/{id}/approve', 'Transfers\Controllers\TransferController@approve', ['auth'], 'transfer.approve'],
['POST', '/transfers/{id}/reject', 'Transfers\Controllers\TransferController@reject', ['auth'], 'transfer.approve'],
['POST', '/transfers/{id}/complete', 'Transfers\Controllers\TransferController@complete',['auth'], 'transfer.approve'],
['GET', '/transfers/preview/{memberId}', 'Transfers\Controllers\TransferController@preview', ['auth'], 'transfer.initiate'],
['POST', '/api/transfers/calculate-fee', 'Transfers\Controllers\TransferController@calculateFee', ['auth'], 'transfer.initiate'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Transfers\Services;
use App\Core\App;
use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Pricing\Services\PricingEngine;
final class SeparationFeeCalculator
{
/**
* Calculate separation/transfer fee based on years since acquisition.
*/
public static function calculate(int $sourceMemberId, ?int $childId = null, ?string $qualificationCode = null): array
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [$sourceMemberId]);
if (!$member) {
return ['error' => 'العضو غير موجود', 'success' => false];
}
// Determine qualification code
if ($qualificationCode === null && $member['qualification_id']) {
$qual = $db->selectOne("SELECT code FROM qualifications WHERE id = ?", [(int) $member['qualification_id']]);
$qualificationCode = $qual['code'] ?? 'high';
}
// Get new membership value at current date
$branchId = (int) $member['branch_id'];
$priceInfo = PricingEngine::getMembershipPrice($branchId, $qualificationCode ?? 'high');
$newMembershipValue = $priceInfo['price'] ?? '0.00';
// Calculate years since acquisition
$acquisitionDate = $member['created_at'] ?? $member['form_date'] ?? date('Y-m-d');
$yearsSince = self::calculateYearsSince($acquisitionDate);
// Get fee percentage based on years
$feePercentage = self::getFeePercentageByYear($yearsSince);
// Calculate separation fee
$separationFee = bcdiv(bcmul($newMembershipValue, (string) $feePercentage, 4), '100', 2);
// Form fee
$formFeeData = RuleEngine::get('FORM_TRANSFER_FEE');
$formFee = $formFeeData['amount'] ?? '570.00';
// Annual subscription (basic member subscription)
$annualSub = '492.00'; // current year rate
$devFeeData = RuleEngine::get('DEVELOPMENT_FEE');
$devFee = $devFeeData['amount'] ?? '35.00';
$annualSubscriptionFee = bcadd($annualSub, $devFee, 2);
$totalFee = bcadd(bcadd($separationFee, $formFee, 2), $annualSubscriptionFee, 2);
return [
'success' => true,
'source_member_id' => $sourceMemberId,
'child_id' => $childId,
'qualification_code' => $qualificationCode,
'new_membership_value' => $newMembershipValue,
'years_since_acquisition'=> $yearsSince,
'fee_percentage' => $feePercentage,
'separation_fee' => $separationFee,
'form_fee' => $formFee,
'annual_subscription_fee'=> $annualSubscriptionFee,
'total_fee' => $totalFee,
'acquisition_date' => $acquisitionDate,
];
}
public static function calculateYearsSince(string $date): int
{
$then = new \DateTime(substr($date, 0, 10));
$now = new \DateTime();
$diff = $now->diff($then);
return max(1, $diff->y + ($diff->m > 0 || $diff->d > 0 ? 1 : 0)); // partial year rounds up
}
public static function getFeePercentageByYear(int $year): string
{
if ($year <= 1) {
$data = RuleEngine::get('SEPARATION_FEE_YEAR_1');
return $data['percentage'] ?? '30.00';
}
if ($year === 2) {
$data = RuleEngine::get('SEPARATION_FEE_YEAR_2');
return $data['percentage'] ?? '20.00';
}
if ($year === 3) {
$data = RuleEngine::get('SEPARATION_FEE_YEAR_3');
return $data['percentage'] ?? '15.00';
}
if ($year === 4) {
$data = RuleEngine::get('SEPARATION_FEE_YEAR_4');
return $data['percentage'] ?? '10.00';
}
if ($year === 5) {
$data = RuleEngine::get('SEPARATION_FEE_YEAR_5');
return $data['percentage'] ?? '5.00';
}
$data = RuleEngine::get('SEPARATION_FEE_YEAR_6_PLUS');
return $data['percentage'] ?? '2.50';
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Transfers\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Archive\Services\ArchiveService;
use App\Modules\Members\Services\MemberNumberGenerator;
use App\Modules\Transfers\Models\TransferRequest;
final class TransferProcessor
{
/**
* Execute a complete transfer/separation.
* 1. Take archive snapshot
* 2. Create new member record
* 3. Assign new membership number
* 4. Record number chain
* 5. Update source records
* 6. Mark transfer complete
*/
public static function execute(int $transferRequestId): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$request = $db->selectOne("SELECT * FROM transfer_requests WHERE id = ?", [$transferRequestId]);
if (!$request) {
return ['success' => false, 'error' => 'طلب التحويل غير موجود'];
}
if ($request['status'] !== 'approved' && $request['status'] !== 'fee_paid') {
return ['success' => false, 'error' => 'طلب التحويل غير مُعتمد أو لم يتم السداد'];
}
$sourceMember = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $request['source_member_id']]);
if (!$sourceMember) {
return ['success' => false, 'error' => 'العضو المصدر غير موجود'];
}
$db->beginTransaction();
try {
// 1. Take archive snapshot of source member
$snapshotId = ArchiveService::takeSnapshot(
'members',
(int) $sourceMember['id'],
$request['transfer_type'],
'لقطة أرشيفية قبل التحويل/الفصل — طلب #' . $transferRequestId,
null,
$sourceMember['membership_number']
);
// 2. Determine the subject of transfer
$subjectName = $sourceMember['full_name_ar'];
$subjectData = $sourceMember;
if ($request['child_id']) {
$child = $db->selectOne("SELECT * FROM children WHERE id = ?", [(int) $request['child_id']]);
if ($child) {
$subjectName = $child['full_name_ar'];
$subjectData = $child;
}
} elseif ($request['spouse_id']) {
$spouse = $db->selectOne("SELECT * FROM spouses WHERE id = ?", [(int) $request['spouse_id']]);
if ($spouse) {
$subjectName = $spouse['full_name_ar'];
$subjectData = $spouse;
}
}
// 3. Create new member record
$newMemberData = [
'full_name_ar' => $subjectData['full_name_ar'] ?? $subjectName,
'full_name_en' => $subjectData['full_name_en'] ?? null,
'national_id' => $subjectData['national_id'] ?? null,
'passport_number' => $subjectData['passport_number'] ?? null,
'id_type' => $subjectData['id_type'] ?? 'national_id',
'date_of_birth' => $subjectData['date_of_birth'],
'age_years' => $subjectData['age_years'] ?? null,
'age_months' => $subjectData['age_months'] ?? null,
'gender' => $subjectData['gender'],
'nationality' => $subjectData['nationality'] ?? 'مصري',
'branch_id' => (int) $sourceMember['branch_id'],
'membership_type' => 'working',
'member_category' => 'working_member',
'status' => 'active',
'qualification_id' => $subjectData['qualification_id'] ?? $sourceMember['qualification_id'],
'phone_mobile' => $subjectData['mobile'] ?? $subjectData['phone_mobile'] ?? $sourceMember['phone_mobile'],
'membership_value' => $request['new_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,
];
$newMemberId = $db->insert('members', $newMemberData);
// 4. Assign new membership number
$newNumber = MemberNumberGenerator::assign($newMemberId);
// 5. Record number chain
ArchiveService::recordNumberTransfer(
$newNumber,
$request['transfer_type'],
'members',
$newMemberId,
null
);
// 6. Update source records
if ($request['child_id']) {
$db->update('children', [
'status' => 'separated',
'classification'=> 'separated',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $request['child_id']]);
} elseif ($request['spouse_id']) {
$db->update('spouses', [
'status' => 'separated',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $request['spouse_id']]);
}
// 7. Mark transfer request complete
$db->update('transfer_requests', [
'target_member_id' => $newMemberId,
'new_membership_number'=> $newNumber,
'archive_snapshot_id' => $snapshotId,
'completed_at' => date('Y-m-d H:i:s'),
'status' => 'completed',
'updated_at' => date('Y-m-d H:i:s'),
'updated_by' => $employee ? (int) $employee->id : null,
], '`id` = ?', [$transferRequestId]);
$db->commit();
EventBus::dispatch('transfer.completed', [
'transfer_id' => $transferRequestId,
'source_member_id' => (int) $request['source_member_id'],
'target_member_id' => $newMemberId,
'new_number' => $newNumber,
'transfer_type' => $request['transfer_type'],
]);
Logger::info("Transfer completed", [
'transfer_id' => $transferRequestId,
'new_member' => $newMemberId,
'new_number' => $newNumber,
]);
return [
'success' => true,
'new_member_id' => $newMemberId,
'new_number' => $newNumber,
'snapshot_id' => $snapshotId,
'transfer_id' => $transferRequestId,
];
} catch (\Throwable $e) {
$db->rollBack();
Logger::error("Transfer failed: " . $e->getMessage(), ['transfer_id' => $transferRequestId]);
return ['success' => false, 'error' => 'فشل في إتمام التحويل: ' . $e->getMessage()];
}
}
}
\ 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;">
<strong>العضو:</strong> <?= e($member['full_name_ar']) ?><strong>رقم العضوية:</strong> <?= e($member['membership_number'] ?? '—') ?>
</div>
<form method="POST" action="/transfers/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="form-group">
<label class="form-label">نوع التحويل <span style="color:#DC2626;">*</span></label>
<select name="transfer_type" id="transfer_type" class="form-select" required>
<option value="">-- اختر --</option>
<option value="child_separation">فصل أبناء</option>
<option value="child_mandatory_25">تحويل وجوبي (25 سنة)</option>
<option value="sports_conversion">تحويل رياضي لعامل</option>
<option value="cross_branch">تحويل بين فروع</option>
</select>
</div>
<div class="form-group" id="child-select" style="display:none;">
<label class="form-label">اختيار الابن/الابنة</label>
<select name="child_id" class="form-select">
<option value="">-- اختر --</option>
<?php foreach ($children as $c): ?>
<option value="<?= (int) $c['id'] ?>"><?= e($c['full_name_ar']) ?><?= (int) ($c['age_years'] ?? 0) ?> سنة (<?= $c['gender'] === 'male' ? 'ذكر' : 'أنثى' ?>)</option>
<?php endforeach; ?>
</select>
</div>
<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>
</div>
<button type="submit" class="btn btn-primary">تقديم الطلب</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</form>
<script>
document.getElementById('transfer_type').addEventListener('change', function() {
var cs = document.getElementById('child-select');
cs.style.display = (this.value === 'child_separation' || this.value === 'child_mandatory_25') ? 'block' : 'none';
});
</script>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>التحويلات والفصل<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/transfers" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div><label class="form-label" style="font-size:12px;">بحث</label><input type="text" name="q" value="<?= e($filters['search'] ?? '') ?>" class="form-input" style="min-width:200px;"></div>
<div><label class="form-label" style="font-size:12px;">الحالة</label><select name="status" class="form-select"><option value="">الكل</option><option value="requested" <?= ($filters['status'] ?? '') === 'requested' ? 'selected' : '' ?>>مقدّم</option><option value="approved" <?= ($filters['status'] ?? '') === 'approved' ? 'selected' : '' ?>>مُعتمد</option><option value="fee_paid" <?= ($filters['status'] ?? '') === 'fee_paid' ? 'selected' : '' ?>>تم السداد</option><option value="completed" <?= ($filters['status'] ?? '') === 'completed' ? 'selected' : '' ?>>مكتمل</option><option value="rejected" <?= ($filters['status'] ?? '') === 'rejected' ? 'selected' : '' ?>>مرفوض</option></select></div>
<div><label class="form-label" style="font-size:12px;">النوع</label><select name="transfer_type" class="form-select"><option value="">الكل</option><option value="child_separation">فصل أبناء</option><option value="child_mandatory_25">تحويل وجوبي 25</option><option value="divorce">طلاق</option><option value="death">وفاة</option><option value="waiver">تنازل</option><option value="sports_conversion">تحويل رياضي</option></select></div>
<button type="submit" class="btn btn-outline">بحث</button>
</form>
</div>
<div class="card"><div class="table-responsive"><table class="data-table"><thead><tr><th>#</th><th>العضو المصدر</th><th>النوع</th><th>الموضوع</th><th>الرسوم</th><th>الرقم الجديد</th><th>الحالة</th><th>التاريخ</th><th>الإجراءات</th></tr></thead><tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= (int) $r['id'] ?></td>
<td><a href="/members/<?= (int) $r['source_member_id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($r['source_name'] ?? '') ?></a><br><small style="color:#9CA3AF;"><?= e($r['source_number'] ?? '') ?></small></td>
<td style="font-size:13px;"><?= e(\App\Modules\Transfers\Models\TransferRequest::getTransferTypeLabel($r['transfer_type'])) ?></td>
<td style="font-size:13px;"><?= e($r['child_name'] ?? $r['spouse_name'] ?? '—') ?></td>
<td style="font-weight:600;"><?= money($r['total_fee'] ?? '0') ?></td>
<td style="font-weight:600;color:#0D7377;"><?= e($r['new_membership_number'] ?? '—') ?></td>
<td><span style="color:<?= match($r['status']) { 'requested' => '#D97706', 'approved' => '#0284C7', 'fee_paid' => '#059669', 'completed' => '#059669', 'rejected' => '#DC2626', default => '#6B7280' } ?>;font-weight:600;"><?= match($r['status']) { 'requested' => 'مقدّم', 'approved' => 'مُعتمد', 'fee_paid' => 'تم السداد', 'completed' => 'مكتمل', 'rejected' => 'مرفوض', default => $r['status'] } ?></span></td>
<td style="font-size:12px;"><?= e(substr($r['created_at'], 0, 10)) ?></td>
<td><a href="/transfers/<?= (int) $r['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?><tr><td colspan="9" style="text-align:center;padding:40px;color:#6B7280;">لا توجد طلبات تحويل</td></tr><?php endif; ?>
</tbody></table></div></div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>معاينة رسوم الفصل<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="padding:20px;">
<h3 style="color:#0D7377;margin-bottom:15px;">معاينة رسوم الفصل — <?= e($member['full_name_ar'] ?? '') ?></h3>
<?php if ($feeCalc['success'] ?? false): ?>
<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;"><?= money($feeCalc['new_membership_value']) ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">سنوات منذ الاكتساب</td><td style="padding:8px 0;"><?= (int) $feeCalc['years_since_acquisition'] ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">النسبة</td><td style="padding:8px 0;"><?= e($feeCalc['fee_percentage']) ?>%</td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">رسوم الفصل</td><td style="padding:8px 0;"><?= money($feeCalc['separation_fee']) ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">رسوم الاستمارة</td><td style="padding:8px 0;"><?= money($feeCalc['form_fee']) ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">الاشتراك السنوي</td><td style="padding:8px 0;"><?= money($feeCalc['annual_subscription_fee']) ?></td></tr>
<tr style="border-top:2px solid #0D7377;"><td style="padding:10px 0;font-weight:700;font-size:16px;">الإجمالي</td><td style="padding:10px 0;font-weight:700;font-size:18px;color:#0D7377;"><?= money($feeCalc['total_fee']) ?></td></tr>
</table>
<a href="/transfers/create/<?= (int) $member['id'] ?>" class="btn btn-primary" style="margin-top:20px;">تقديم طلب التحويل</a>
<?php else: ?>
<p style="color:#DC2626;"><?= e($feeCalc['error'] ?? 'خطأ في الحساب') ?></p>
<?php endif; ?>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>طلب تحويل #<?= (int) $transfer['id'] ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/transfers" class="btn btn-outline">← العودة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin-bottom:15px;">بيانات الطلب</h4>
<table style="width:100%;font-size:14px;">
<tr><td style="padding:6px 0;color:#6B7280;width:40%;">رقم الطلب</td><td style="padding:6px 0;font-weight:700;">#<?= (int) $transfer['id'] ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">نوع التحويل</td><td style="padding:6px 0;font-weight:600;"><?= e(\App\Modules\Transfers\Models\TransferRequest::getTransferTypeLabel($transfer['transfer_type'])) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">العضو المصدر</td><td style="padding:6px 0;"><a href="/members/<?= (int) $transfer['source_member_id'] ?>" style="color:#0D7377;"><?= e($transfer['source_name']) ?></a> (<?= e($transfer['source_number'] ?? '—') ?>)</td></tr>
<?php if ($transfer['child_name']): ?><tr><td style="padding:6px 0;color:#6B7280;">الابن/الابنة</td><td style="padding:6px 0;"><?= e($transfer['child_name']) ?></td></tr><?php endif; ?>
<?php if ($transfer['spouse_name']): ?><tr><td style="padding:6px 0;color:#6B7280;">الزوج/الزوجة</td><td style="padding:6px 0;"><?= e($transfer['spouse_name']) ?></td></tr><?php endif; ?>
<tr><td style="padding:6px 0;color:#6B7280;">الحالة</td><td style="padding:6px 0;font-weight:700;color:<?= match($transfer['status']) { 'completed' => '#059669', 'rejected' => '#DC2626', default => '#D97706' } ?>;"><?= match($transfer['status']) { 'requested' => 'مقدّم', 'approved' => 'مُعتمد', 'fee_paid' => 'تم السداد', 'completed' => 'مكتمل', 'rejected' => 'مرفوض', default => $transfer['status'] } ?></td></tr>
<?php if ($transfer['target_name']): ?><tr><td style="padding:6px 0;color:#6B7280;">العضو الجديد</td><td style="padding:6px 0;"><a href="/members/<?= (int) $transfer['target_member_id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($transfer['target_name']) ?></a></td></tr><?php endif; ?>
<?php if ($transfer['new_membership_number']): ?><tr><td style="padding:6px 0;color:#6B7280;">رقم العضوية الجديد</td><td style="padding:6px 0;font-weight:700;color:#0D7377;font-size:18px;"><?= e($transfer['new_membership_number']) ?></td></tr><?php endif; ?>
<?php if ($transfer['notes']): ?><tr><td style="padding:6px 0;color:#6B7280;">ملاحظات</td><td style="padding:6px 0;"><?= e($transfer['notes']) ?></td></tr><?php endif; ?>
</table>
</div>
<div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin-bottom:15px;">تفاصيل الرسوم</h4>
<table style="width:100%;font-size:14px;">
<tr><td style="padding:6px 0;color:#6B7280;">قيمة العضوية الجديدة</td><td style="padding:6px 0;"><?= money($transfer['new_membership_value'] ?? '0') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">سنوات منذ الاكتساب</td><td style="padding:6px 0;"><?= (int) ($transfer['years_since_acquisition'] ?? 0) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">نسبة رسوم الفصل</td><td style="padding:6px 0;"><?= e($transfer['fee_percentage'] ?? '0') ?>%</td></tr>
<tr><td style="padding:8px 0;color:#6B7280;border-top:1px solid #E5E7EB;">رسوم الفصل</td><td style="padding:8px 0;border-top:1px solid #E5E7EB;"><?= money($transfer['separation_fee'] ?? '0') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">رسوم الاستمارة</td><td style="padding:6px 0;"><?= money($transfer['form_fee'] ?? '0') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الاشتراك السنوي</td><td style="padding:6px 0;"><?= money($transfer['annual_subscription_fee'] ?? '0') ?></td></tr>
<tr><td style="padding:10px 0;color:#1A1A2E;font-weight:700;font-size:16px;border-top:2px solid #0D7377;">الإجمالي</td><td style="padding:10px 0;font-weight:700;font-size:18px;color:#0D7377;border-top:2px solid #0D7377;"><?= money($transfer['total_fee'] ?? '0') ?></td></tr>
</table>
<?php if ($transfer['status'] === 'requested'): ?>
<div style="margin-top:20px;display:flex;gap:10px;">
<form method="POST" action="/transfers/<?= (int) $transfer['id'] ?>/approve" style="flex:1;">
<?= csrf_field() ?>
<textarea name="board_decision_notes" class="form-textarea" rows="2" placeholder="ملاحظات المجلس..." style="margin-bottom:10px;"></textarea>
<button type="submit" class="btn btn-primary" style="width:100%;" onclick="return confirm('هل تريد الموافقة على هذا الطلب؟')">✓ موافقة</button>
</form>
<form method="POST" action="/transfers/<?= (int) $transfer['id'] ?>/reject" style="flex:1;">
<?= csrf_field() ?>
<textarea name="reject_reason" class="form-textarea" rows="2" placeholder="سبب الرفض..." style="margin-bottom:10px;"></textarea>
<button type="submit" class="btn" style="width:100%;background:#DC2626;color:#fff;border:none;padding:10px;border-radius:6px;cursor:pointer;" onclick="return confirm('هل تريد رفض هذا الطلب؟')">✗ رفض</button>
</form>
</div>
<?php endif; ?>
<?php if ($transfer['status'] === 'approved' || $transfer['status'] === 'fee_paid'): ?>
<div style="margin-top:20px;">
<form method="POST" action="/transfers/<?= (int) $transfer['id'] ?>/complete">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" style="width:100%;padding:12px;font-size:16px;" onclick="return confirm('⚠ هذا الإجراء نهائي — سيتم إنشاء عضوية جديدة وأرشفة البيانات القديمة. متأكد؟')">🔄 إتمام التحويل وإنشاء العضوية الجديدة</button>
</form>
</div>
<?php endif; ?>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
MenuRegistry::register('transfers', [
'label_ar' => 'التحويلات والفصل',
'label_en' => 'Transfers & Separations',
'icon' => 'swap',
'route' => '/transfers',
'permission' => 'transfer.view',
'parent' => null,
'order' => 700,
'children' => [
['label_ar' => 'طلبات التحويل', 'label_en' => 'Transfer Requests', 'route' => '/transfers', 'permission' => 'transfer.view', 'order' => 1],
['label_ar' => 'حالات الطلاق', 'label_en' => 'Divorce Cases', 'route' => '/divorce', 'permission' => 'transfer.view', 'order' => 2],
['label_ar' => 'حالات الوفاة', 'label_en' => 'Death Cases', 'route' => '/death', 'permission' => 'transfer.view', 'order' => 3],
['label_ar' => 'طلبات التنازل', 'label_en' => 'Waiver Requests', 'route' => '/waivers', 'permission' => 'waiver.view', 'order' => 4],
],
]);
PermissionRegistry::register('transfers', [
'transfer.initiate' => ['ar' => 'بدء تحويل/فصل', 'en' => 'Initiate Transfer'],
'transfer.approve' => ['ar' => 'موافقة تحويل/فصل', 'en' => 'Approve Transfer'],
'transfer.view' => ['ar' => 'عرض التحويلات', 'en' => 'View Transfers'],
'separation.initiate' => ['ar' => 'بدء فصل', 'en' => 'Initiate Separation'],
'separation.approve' => ['ar' => 'موافقة فصل', 'en' => 'Approve Separation'],
'separation.view' => ['ar' => 'عرض الفصل', 'en' => 'View Separations'],
'waiver.initiate' => ['ar' => 'بدء تنازل', 'en' => 'Initiate Waiver'],
'waiver.approve' => ['ar' => 'موافقة تنازل', 'en' => 'Approve Waiver'],
'waiver.view' => ['ar' => 'عرض التنازلات', 'en' => 'View Waivers'],
]);
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Waiver\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Waiver\Models\WaiverRequest;
use App\Modules\Waiver\Services\WaiverProcessor;
use App\Modules\Rules\Services\RuleEngine;
class WaiverController extends Controller
{
public function index(Request $request): Response
{
$filters = ['search' => trim((string) $request->get('q', '')), 'status' => $request->get('status', '')];
$page = max(1, (int) $request->get('page', 1));
$result = WaiverRequest::search($filters, 25, $page);
return $this->view('Waiver.Views.index', ['rows' => $result['data'], 'pagination' => $result['pagination'], 'filters' => $filters]);
}
public function create(Request $request, string $memberId): Response
{
$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('العضو غير موجود');
$membershipValue = $member['membership_value'] ?? '0.00';
$waiverPctData = RuleEngine::get('WAIVER_FEE');
$waiverPct = $waiverPctData['percentage'] ?? '30.00';
$waiverFee = bcdiv(bcmul($membershipValue, $waiverPct, 4), '100', 2);
// Count dependents
$spouseCount = (int) ($db->selectOne("SELECT COUNT(*) as cnt FROM spouses WHERE member_id = ? AND is_archived = 0", [(int) $memberId])['cnt'] ?? 0);
$childCount = (int) ($db->selectOne("SELECT COUNT(*) as cnt FROM children WHERE member_id = ? AND is_archived = 0", [(int) $memberId])['cnt'] ?? 0);
$tempCount = (int) ($db->selectOne("SELECT COUNT(*) as cnt FROM temporary_members WHERE member_id = ? AND is_archived = 0", [(int) $memberId])['cnt'] ?? 0);
$totalDependents = $spouseCount + $childCount + $tempCount;
return $this->view('Waiver.Views.create', [
'member' => $member,
'waiver_pct' => $waiverPct,
'waiver_fee' => $waiverFee,
'total_dependents'=> $totalDependents,
]);
}
public function store(Request $request, string $memberId): Response
{
$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('العضو غير موجود');
if (!$member['membership_number']) return $this->redirect("/members/{$memberId}")->withError('العضو ليس لديه رقم عضوية');
$membershipValue = $member['membership_value'] ?? '0.00';
$waiverPctData = RuleEngine::get('WAIVER_FEE');
$waiverPct = $waiverPctData['percentage'] ?? '30.00';
$waiverFee = bcdiv(bcmul($membershipValue, $waiverPct, 4), '100', 2);
$spouseCount = (int) ($db->selectOne("SELECT COUNT(*) as cnt FROM spouses WHERE member_id = ? AND is_archived = 0", [(int) $memberId])['cnt'] ?? 0);
$childCount = (int) ($db->selectOne("SELECT COUNT(*) as cnt FROM children WHERE member_id = ? AND is_archived = 0", [(int) $memberId])['cnt'] ?? 0);
$tempCount = (int) ($db->selectOne("SELECT COUNT(*) as cnt FROM temporary_members WHERE member_id = ? AND is_archived = 0", [(int) $memberId])['cnt'] ?? 0);
$waiver = WaiverRequest::create([
'source_member_id' => (int) $memberId,
'membership_number' => $member['membership_number'],
'membership_value_at_waiver' => $membershipValue,
'waiver_fee_percentage' => $waiverPct,
'waiver_fee_amount' => $waiverFee,
'original_dependent_count' => $spouseCount + $childCount + $tempCount,
'board_approval_required' => 1,
'status' => 'requested',
'notes' => trim($request->post('notes', '')) ?: null,
]);
EventBus::dispatch('waiver.requested', ['waiver_id' => (int) $waiver->id, 'member_id' => (int) $memberId]);
return $this->redirect("/waivers/{$waiver->id}")->withSuccess('تم تقديم طلب التنازل — الرسوم: ' . money($waiverFee) . ' (' . $waiverPct . '%)');
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$waiver = $db->selectOne(
"SELECT wr.*, m.full_name_ar as source_name, tm.full_name_ar as target_name, e.full_name_ar as approved_by_name
FROM waiver_requests wr JOIN members m ON m.id = wr.source_member_id
LEFT JOIN members tm ON tm.id = wr.target_member_id
LEFT JOIN employees e ON e.id = wr.approved_by
WHERE wr.id = ?",
[(int) $id]
);
if (!$waiver) return $this->redirect('/waivers')->withError('الطلب غير موجود');
return $this->view('Waiver.Views.show', ['waiver' => $waiver]);
}
public function approve(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$waiver = $db->selectOne("SELECT * FROM waiver_requests WHERE id = ? AND status = 'requested'", [(int) $id]);
if (!$waiver) return $this->redirect('/waivers')->withError('الطلب غير صالح');
$employee = App::getInstance()->currentEmployee();
$boardRef = trim($request->post('board_decision_reference', ''));
$db->update('waiver_requests', [
'status' => 'approved',
'approved_by' => $employee ? (int) $employee->id : null,
'approved_at' => date('Y-m-d H:i:s'),
'board_decision_reference' => $boardRef ?: null,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
return $this->redirect("/waivers/{$id}")->withSuccess('تمت الموافقة على طلب التنازل');
}
public function complete(Request $request, string $id): Response
{
// The target member must be created first (via member creation flow)
// and linked to the waiver
$db = App::getInstance()->db();
$targetMemberId = (int) $request->post('target_member_id', 0);
if ($targetMemberId > 0) {
$db->update('waiver_requests', [
'target_member_id' => $targetMemberId,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
}
$result = WaiverProcessor::execute((int) $id);
if (!$result['success']) return $this->redirect("/waivers/{$id}")->withError($result['error']);
return $this->redirect("/waivers/{$id}")->withSuccess('تم إتمام التنازل — العضوية نُقلت بنجاح');
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Waiver\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class WaiverRequest extends Model
{
protected static string $table = 'waiver_requests';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $dispatchEvents = true;
protected static array $fillable = [
'source_member_id', 'target_member_id', 'membership_number',
'membership_value_at_waiver', 'waiver_fee_percentage', 'waiver_fee_amount',
'original_dependent_count', 'new_dependent_count',
'board_approval_required', 'board_decision_reference',
'approved_by', 'approved_at', 'annual_renewal_paid',
'archive_snapshot_id', 'workflow_instance_id', 'status', 'notes',
];
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['status'])) { $where .= ' AND wr.status = ?'; $params[] = $filters['status']; }
if (!empty($filters['search'])) {
$where .= ' AND (m.full_name_ar LIKE ? OR wr.membership_number LIKE ?)';
$s = '%' . $filters['search'] . '%'; $params[] = $s; $params[] = $s;
}
$countRow = $db->selectOne("SELECT COUNT(*) as cnt FROM waiver_requests wr JOIN members m ON m.id = wr.source_member_id WHERE {$where}", $params);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT wr.*, m.full_name_ar as source_name, tm.full_name_ar as target_name
FROM waiver_requests wr JOIN members m ON m.id = wr.source_member_id
LEFT JOIN members tm ON tm.id = wr.target_member_id
WHERE {$where} ORDER BY wr.created_at DESC LIMIT {$perPage} OFFSET {$offset}", $params
);
return ['data' => $rows, 'pagination' => Pagination::paginate($total, $perPage, $page)];
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/waivers', 'Waiver\Controllers\WaiverController@index', ['auth'], 'waiver.view'],
['GET', '/waivers/create/{memberId}', 'Waiver\Controllers\WaiverController@create', ['auth'], 'waiver.initiate'],
['POST', '/waivers/store/{memberId}', 'Waiver\Controllers\WaiverController@store', ['auth'], 'waiver.initiate'],
['GET', '/waivers/{id}', 'Waiver\Controllers\WaiverController@show', ['auth'], 'waiver.view'],
['POST', '/waivers/{id}/approve', 'Waiver\Controllers\WaiverController@approve', ['auth'], 'waiver.approve'],
['POST', '/waivers/{id}/complete', 'Waiver\Controllers\WaiverController@complete',['auth'], 'waiver.approve'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Waiver\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Archive\Services\ArchiveService;
final class WaiverProcessor
{
public static function execute(int $waiverId): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$waiver = $db->selectOne("SELECT * FROM waiver_requests WHERE id = ?", [$waiverId]);
if (!$waiver || !in_array($waiver['status'], ['approved', 'fee_paid'])) {
return ['success' => false, 'error' => 'طلب التنازل غير صالح أو غير مُعتمد'];
}
if (!$waiver['target_member_id']) {
return ['success' => false, 'error' => 'لم يتم تحديد العضو المستفيد'];
}
$sourceMember = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $waiver['source_member_id']]);
if (!$sourceMember) return ['success' => false, 'error' => 'العضو المصدر غير موجود'];
$db->beginTransaction();
try {
// Archive snapshot
$snapshotId = ArchiveService::takeSnapshot('members', (int) $sourceMember['id'], 'waiver', 'تنازل — طلب #' . $waiverId);
// Transfer membership number to target member
$db->update('members', [
'membership_number' => $waiver['membership_number'],
'status' => 'active',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $waiver['target_member_id']]);
// Archive source member (remove number, set archived)
$db->update('members', [
'membership_number' => null,
'status' => 'waived',
'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
'archived_by' => $employee ? (int) $employee->id : null,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $waiver['source_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',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$waiverId]);
$db->commit();
EventBus::dispatch('waiver.completed', [
'waiver_id' => $waiverId,
'source_member_id' => (int) $waiver['source_member_id'],
'target_member_id' => (int) $waiver['target_member_id'],
'membership_number'=> $waiver['membership_number'],
]);
return ['success' => true, 'snapshot_id' => $snapshotId];
} catch (\Throwable $e) {
$db->rollBack();
Logger::error("Waiver failed: " . $e->getMessage());
return ['success' => false, 'error' => $e->getMessage()];
}
}
}
\ 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="padding:20px;margin-bottom:20px;">
<div style="padding:15px;background:#FFF7ED;border:1px solid #FED7AA;border-radius:8px;margin-bottom:20px;">
<strong style="color:#D97706;">⚠ التنازل عن العضوية:</strong> يتطلب موافقة مجلس الأمناء. العضو المستفيد يحصل على نفس رقم العضوية. عدد التابعين الجدد لا يتجاوز عدد التابعين الأصليين.
</div>
<table style="width:100%;max-width:500px;font-size:14px;">
<tr><td style="padding:6px 0;color:#6B7280;">العضو</td><td style="padding:6px 0;font-weight:600;"><?= e($member['full_name_ar']) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">رقم العضوية</td><td style="padding:6px 0;font-weight:700;"><?= e($member['membership_number'] ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">قيمة العضوية</td><td style="padding:6px 0;"><?= money($member['membership_value'] ?? '0') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">نسبة التنازل</td><td style="padding:6px 0;"><?= e($waiver_pct) ?>%</td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">عدد التابعين</td><td style="padding:6px 0;"><?= (int) $total_dependents ?></td></tr>
<tr style="border-top:2px solid #0D7377;"><td style="padding:10px 0;font-weight:700;font-size:16px;">رسوم التنازل</td><td style="padding:10px 0;font-weight:700;font-size:18px;color:#0D7377;"><?= money($waiver_fee) ?></td></tr>
</table>
</div>
<form method="POST" action="/waivers/store/<?= (int) $member['id'] ?>">
<?= csrf_field() ?>
<div class="card" style="padding:20px;margin-bottom:15px;">
<div class="form-group"><label class="form-label">ملاحظات</label><textarea name="notes" class="form-textarea" rows="3"></textarea></div>
</div>
<button type="submit" class="btn btn-primary" onclick="return confirm('هل تريد تقديم طلب التنازل؟')">تقديم طلب التنازل</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>طلبات التنازل<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card"><div class="table-responsive"><table class="data-table"><thead><tr><th>#</th><th>العضو</th><th>رقم العضوية</th><th>الرسوم</th><th>المستفيد</th><th>الحالة</th><th>الإجراءات</th></tr></thead><tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= (int) $r['id'] ?></td>
<td><?= e($r['source_name']) ?></td>
<td style="font-weight:600;"><?= e($r['membership_number']) ?></td>
<td style="font-weight:600;"><?= money($r['waiver_fee_amount']) ?></td>
<td><?= e($r['target_name'] ?? '—') ?></td>
<td><span style="color:<?= match($r['status']) { 'completed' => '#059669', 'approved' => '#0284C7', 'rejected' => '#DC2626', default => '#D97706' } ?>;font-weight:600;"><?= match($r['status']) { 'requested' => 'مقدّم', 'approved' => 'مُعتمد', 'completed' => 'مكتمل', 'rejected' => 'مرفوض', default => $r['status'] } ?></span></td>
<td><a href="/waivers/<?= (int) $r['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?><tr><td colspan="7" style="text-align:center;padding:40px;color:#6B7280;">لا توجد طلبات تنازل</td></tr><?php endif; ?>
</tbody></table></div></div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>طلب تنازل #<?= (int) $waiver['id'] ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="padding: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($waiver['source_name']) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">رقم العضوية</td><td style="padding:6px 0;font-weight:700;"><?= e($waiver['membership_number']) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">قيمة العضوية وقت التنازل</td><td style="padding:6px 0;"><?= money($waiver['membership_value_at_waiver']) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">النسبة</td><td style="padding:6px 0;"><?= e($waiver['waiver_fee_percentage']) ?>%</td></tr>
<tr><td style="padding:6px 0;color:#6B7280;font-weight:700;">رسوم التنازل</td><td style="padding:6px 0;font-weight:700;color:#0D7377;font-size:18px;"><?= money($waiver['waiver_fee_amount']) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">عدد التابعين الأصلي</td><td style="padding:6px 0;"><?= (int) $waiver['original_dependent_count'] ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">المستفيد</td><td style="padding:6px 0;"><?= e($waiver['target_name'] ?? 'لم يُحدد بعد') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الحالة</td><td style="padding:6px 0;font-weight:700;color:<?= match($waiver['status']) { 'completed' => '#059669', 'approved' => '#0284C7', default => '#D97706' } ?>;"><?= match($waiver['status']) { 'requested' => 'في انتظار الموافقة', 'approved' => 'مُعتمد', 'completed' => 'مكتمل', 'rejected' => 'مرفوض', default => $waiver['status'] } ?></td></tr>
<?php if ($waiver['board_decision_reference']): ?><tr><td style="padding:6px 0;color:#6B7280;">مرجع قرار المجلس</td><td style="padding:6px 0;"><?= e($waiver['board_decision_reference']) ?></td></tr><?php endif; ?>
</table>
<?php if ($waiver['status'] === 'requested'): ?>
<div style="margin-top:20px;display:flex;gap:10px;">
<form method="POST" action="/waivers/<?= (int) $waiver['id'] ?>/approve">
<?= csrf_field() ?>
<input type="text" name="board_decision_reference" placeholder="مرجع قرار المجلس" class="form-input" style="margin-bottom:10px;">
<button type="submit" class="btn btn-primary" onclick="return confirm('موافقة على التنازل؟')">✓ موافقة مجلس الأمناء</button>
</form>
</div>
<?php endif; ?>
<?php if ($waiver['status'] === 'approved' && !$waiver['target_member_id']): ?>
<div style="margin-top:20px;padding:15px;background:#EFF6FF;border:1px solid #BFDBFE;border-radius:8px;">
<strong>الخطوة التالية:</strong> يجب إنشاء عضوية جديدة للمستفيد أولاً، ثم ربطها بهذا الطلب وإتمام التنازل.
<form method="POST" action="/waivers/<?= (int) $waiver['id'] ?>/complete" style="margin-top:10px;">
<?= csrf_field() ?>
<input type="number" name="target_member_id" placeholder="رقم العضو المستفيد (ID)" class="form-input" style="width:200px;margin-bottom:10px;" required>
<button type="submit" class="btn btn-primary" onclick="return confirm('⚠ إتمام التنازل نهائياً. متأكد؟')">إتمام التنازل</button>
</form>
</div>
<?php endif; ?>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
// Waiver module bootstraps under Transfers menu (already registered)
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `transfer_requests` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`source_member_id` BIGINT UNSIGNED NOT NULL,
`target_member_id` BIGINT UNSIGNED NULL,
`transfer_type` VARCHAR(50) NOT NULL,
`child_id` BIGINT UNSIGNED NULL,
`spouse_id` BIGINT UNSIGNED NULL,
`source_membership_number` VARCHAR(20) NULL,
`new_membership_number` VARCHAR(20) NULL,
`new_membership_value` DECIMAL(15,2) NULL,
`years_since_acquisition` INT UNSIGNED NULL,
`qualification_code` VARCHAR(20) NULL,
`fee_percentage` DECIMAL(5,2) NULL,
`separation_fee` DECIMAL(15,2) NULL,
`form_fee` DECIMAL(15,2) NULL,
`annual_subscription_fee` DECIMAL(15,2) NULL,
`total_fee` DECIMAL(15,2) NULL,
`archive_snapshot_id` BIGINT UNSIGNED NULL,
`workflow_instance_id` BIGINT UNSIGNED NULL,
`board_decision_notes` TEXT NULL,
`approved_by` BIGINT UNSIGNED NULL,
`approved_at` TIMESTAMP NULL DEFAULT NULL,
`completed_at` TIMESTAMP NULL DEFAULT NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'requested',
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
INDEX `idx_transfers_source` (`source_member_id`),
INDEX `idx_transfers_target` (`target_member_id`),
INDEX `idx_transfers_type` (`transfer_type`),
INDEX `idx_transfers_status` (`status`),
INDEX `idx_transfers_child` (`child_id`),
INDEX `idx_transfers_spouse` (`spouse_id`),
CONSTRAINT `fk_transfers_source` FOREIGN KEY (`source_member_id`) REFERENCES `members`(`id`),
CONSTRAINT `fk_transfers_target` FOREIGN KEY (`target_member_id`) REFERENCES `members`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_transfers_child` FOREIGN KEY (`child_id`) REFERENCES `children`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_transfers_spouse` FOREIGN KEY (`spouse_id`) REFERENCES `spouses`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_transfers_snapshot` FOREIGN KEY (`archive_snapshot_id`) REFERENCES `archive_snapshots`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_transfers_workflow` FOREIGN KEY (`workflow_instance_id`) REFERENCES `workflow_instances`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `transfer_requests`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `divorce_cases` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`member_id` BIGINT UNSIGNED NOT NULL,
`spouse_id` BIGINT UNSIGNED NOT NULL,
`divorce_date` DATE NOT NULL,
`divorce_case_type` VARCHAR(50) NOT NULL,
`request_date` DATE NOT NULL,
`membership_acquisition_date` DATE NULL,
`years_of_membership` INT UNSIGNED NULL,
`has_children_on_membership` TINYINT(1) NOT NULL DEFAULT 0,
`fee_percentage` DECIMAL(5,2) NULL,
`fee_amount` DECIMAL(15,2) NULL,
`spouse_new_member_id` BIGINT UNSIGNED NULL,
`spouse_new_membership_number` VARCHAR(20) NULL,
`children_assignment_json` JSON NULL,
`archive_snapshot_id` BIGINT UNSIGNED NULL,
`workflow_instance_id` BIGINT UNSIGNED NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'submitted',
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
INDEX `idx_divorce_member` (`member_id`),
INDEX `idx_divorce_spouse` (`spouse_id`),
INDEX `idx_divorce_status` (`status`),
INDEX `idx_divorce_date` (`divorce_date`),
CONSTRAINT `fk_divorce_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`),
CONSTRAINT `fk_divorce_spouse` FOREIGN KEY (`spouse_id`) REFERENCES `spouses`(`id`),
CONSTRAINT `fk_divorce_new_member` FOREIGN KEY (`spouse_new_member_id`) REFERENCES `members`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_divorce_snapshot` FOREIGN KEY (`archive_snapshot_id`) REFERENCES `archive_snapshots`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_divorce_workflow` FOREIGN KEY (`workflow_instance_id`) REFERENCES `workflow_instances`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `divorce_cases`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `death_cases` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`member_id` BIGINT UNSIGNED NOT NULL,
`deceased_type` VARCHAR(30) NOT NULL,
`death_date` DATE NOT NULL,
`death_certificate_number` VARCHAR(100) NULL,
`death_certificate_path` VARCHAR(500) NULL,
`spouse_id` BIGINT UNSIGNED NULL,
`child_id` BIGINT UNSIGNED NULL,
`transferred_to_member_id` BIGINT UNSIGNED NULL,
`same_membership_number` TINYINT(1) NOT NULL DEFAULT 0,
`fee_amount` DECIMAL(15,2) NULL,
`archive_snapshot_id` BIGINT UNSIGNED NULL,
`workflow_instance_id` BIGINT UNSIGNED NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'recorded',
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
INDEX `idx_death_member` (`member_id`),
INDEX `idx_death_status` (`status`),
INDEX `idx_death_date` (`death_date`),
CONSTRAINT `fk_death_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`),
CONSTRAINT `fk_death_spouse` FOREIGN KEY (`spouse_id`) REFERENCES `spouses`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_death_child` FOREIGN KEY (`child_id`) REFERENCES `children`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_death_transferred` FOREIGN KEY (`transferred_to_member_id`) REFERENCES `members`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_death_snapshot` FOREIGN KEY (`archive_snapshot_id`) REFERENCES `archive_snapshots`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_death_workflow` FOREIGN KEY (`workflow_instance_id`) REFERENCES `workflow_instances`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `death_cases`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `waiver_requests` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`source_member_id` BIGINT UNSIGNED NOT NULL,
`target_member_id` BIGINT UNSIGNED NULL,
`membership_number` VARCHAR(20) NOT NULL,
`membership_value_at_waiver` DECIMAL(15,2) NOT NULL,
`waiver_fee_percentage` DECIMAL(5,2) NOT NULL DEFAULT 30.00,
`waiver_fee_amount` DECIMAL(15,2) NOT NULL,
`original_dependent_count` INT UNSIGNED NOT NULL DEFAULT 0,
`new_dependent_count` INT UNSIGNED NULL,
`board_approval_required` TINYINT(1) NOT NULL DEFAULT 1,
`board_decision_reference` VARCHAR(100) NULL,
`approved_by` BIGINT UNSIGNED NULL,
`approved_at` TIMESTAMP NULL DEFAULT NULL,
`annual_renewal_paid` TINYINT(1) NOT NULL DEFAULT 0,
`archive_snapshot_id` BIGINT UNSIGNED NULL,
`workflow_instance_id` BIGINT UNSIGNED NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'requested',
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
INDEX `idx_waiver_source` (`source_member_id`),
INDEX `idx_waiver_target` (`target_member_id`),
INDEX `idx_waiver_number` (`membership_number`),
INDEX `idx_waiver_status` (`status`),
CONSTRAINT `fk_waiver_source` FOREIGN KEY (`source_member_id`) REFERENCES `members`(`id`),
CONSTRAINT `fk_waiver_target` FOREIGN KEY (`target_member_id`) REFERENCES `members`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_waiver_snapshot` FOREIGN KEY (`archive_snapshot_id`) REFERENCES `archive_snapshots`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_waiver_workflow` FOREIGN KEY (`workflow_instance_id`) REFERENCES `workflow_instances`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `waiver_requests`",
];
\ No newline at end of file
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