Commit 6159c91e authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix divorce module: eligibility date, child fees, rule correction, full cycle

- Fix DIVORCE_CHILD_UNDER_12 rule: 15% → 5% per bylaws
- Fix eligibility: use spouse's join_date (not member's created_at) for 5-year check
- Add suggested percentage based on spouse_order (50% first, 75% second)
- Implement children transfer with age-based fees on completion
- Board approval now includes children selection with fee preview
- Complete action transfers selected children to new membership
- Show view displays spouse order, join date, children assignment details
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 2c199906
......@@ -76,12 +76,13 @@ class DivorceController extends Controller
return $this->redirect("/divorce/create/{$memberId}")->withError('قسيمة الطلاق مطلوبة');
}
// Validate basic eligibility
$feeCalc = DivorceFeeCalculator::calculate((int) $memberId, $spouseId, $divorceDate);
if (!($feeCalc['success'] ?? false)) {
return $this->redirect("/divorce/create/{$memberId}")->withError($feeCalc['error'] ?? 'خطأ');
}
$spouse = $db->selectOne("SELECT * FROM spouses WHERE id = ?", [$spouseId]);
$case = DivorceCase::create([
'member_id' => (int) $memberId,
'spouse_id' => $spouseId,
......@@ -90,7 +91,7 @@ class DivorceController extends Controller
'spouse_qualification_id' => $qualificationId,
'divorce_case_type' => $feeCalc['case_type'],
'request_date' => date('Y-m-d'),
'membership_acquisition_date' => substr($member['created_at'] ?? '', 0, 10),
'membership_acquisition_date' => substr($spouse['join_date'] ?? $spouse['created_at'] ?? '', 0, 10),
'years_of_membership' => $feeCalc['years_of_membership'],
'has_children_on_membership' => $feeCalc['has_children'] ? 1 : 0,
'status' => 'board_review',
......@@ -127,19 +128,36 @@ class DivorceController extends Controller
$percentageFee = bcdiv(bcmul($membershipValue, $feePercentage, 4), '100', 2);
$formFee = DivorceFeeCalculator::getFormFee();
$annualSub = DivorceFeeCalculator::getAnnualSubscription();
$totalFee = bcadd(bcadd($percentageFee, $formFee, 2), $annualSub, 2);
// Children transfer fees (if any selected)
$childrenSelected = $request->post('children', []);
$childFeeTotal = '0.00';
$childrenAssignment = [];
if (!empty($childrenSelected) && is_array($childrenSelected)) {
$childCalc = DivorceFeeCalculator::calculateChildFees((int) $case['member_id'], $membershipValue);
foreach ($childCalc['children'] as $child) {
if (in_array((string) $child['id'], $childrenSelected, true)) {
$childFeeTotal = bcadd($childFeeTotal, $child['fee'], 2);
$childrenAssignment[] = $child;
}
}
}
$totalFee = bcadd(bcadd(bcadd($percentageFee, $formFee, 2), $annualSub, 2), $childFeeTotal, 2);
$db->update('divorce_cases', [
'fee_percentage' => $feePercentage,
'fee_amount' => $percentageFee,
'form_fee' => $formFee,
'fee_percentage' => $feePercentage,
'fee_amount' => $percentageFee,
'form_fee' => $formFee,
'annual_subscription_fee' => $annualSub,
'total_fee' => $totalFee,
'board_decision_number' => $boardDecisionNumber ?: null,
'board_decision_date' => $boardDecisionDate ?: null,
'board_notes' => $boardNotes ?: null,
'status' => 'board_approved',
'updated_at' => date('Y-m-d H:i:s'),
'total_fee' => $totalFee,
'board_decision_number' => $boardDecisionNumber ?: null,
'board_decision_date' => $boardDecisionDate ?: null,
'board_notes' => $boardNotes ?: null,
'children_assignment_json' => !empty($childrenAssignment) ? json_encode($childrenAssignment, JSON_UNESCAPED_UNICODE) : null,
'status' => 'board_approved',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
// Auto-send payment request to cashier
......@@ -149,9 +167,12 @@ class DivorceController extends Controller
'رسوم الطلاق: ' . money($percentageFee),
'رسوم استمارة (570): ' . money($formFee),
'اشتراك سنوي: ' . money($annualSub),
'═══════════════════════════',
'الإجمالي: ' . money($totalFee),
];
if (bccomp($childFeeTotal, '0', 2) > 0) {
$breakdown[] = 'رسوم ضم أبناء: ' . money($childFeeTotal) . ' (' . count($childrenAssignment) . ' أبناء)';
}
$breakdown[] = '═══════════════════════════';
$breakdown[] = 'الإجمالي: ' . money($totalFee);
PaymentRequestService::createRequest([
'member_id' => (int) $case['member_id'],
......@@ -196,7 +217,8 @@ class DivorceController extends Controller
$db = App::getInstance()->db();
$case = $db->selectOne(
"SELECT dc.*, m.full_name_ar as member_name, m.membership_number, m.membership_value,
s.full_name_ar as spouse_name, q.name_ar as qualification_name
s.full_name_ar as spouse_name, s.spouse_order, s.join_date as spouse_join_date,
q.name_ar as qualification_name
FROM divorce_cases dc
JOIN members m ON m.id = dc.member_id
JOIN spouses s ON s.id = dc.spouse_id
......@@ -206,7 +228,25 @@ class DivorceController extends Controller
);
if (!$case) return $this->redirect('/divorce')->withError('الحالة غير موجودة');
return $this->view('Divorce.Views.show', ['case' => $case]);
$children = [];
$childFees = null;
$suggestedPercentage = null;
if ($case['status'] === 'board_review') {
$feeCalc = DivorceFeeCalculator::calculate((int) $case['member_id'], (int) $case['spouse_id'], $case['divorce_date']);
if ($feeCalc['success'] ?? false) {
$suggestedPercentage = $feeCalc['suggested_percentage'] ?? null;
}
$childFees = DivorceFeeCalculator::calculateChildFees((int) $case['member_id'], $case['membership_value'] ?? '0');
$children = $childFees['children'] ?? [];
}
return $this->view('Divorce.Views.show', [
'case' => $case,
'children' => $children,
'childFees' => $childFees,
'suggestedPercentage' => $suggestedPercentage,
]);
}
public function pay(Request $request, string $id): Response
......@@ -267,6 +307,7 @@ class DivorceController extends Controller
$spouse = $db->selectOne("SELECT * FROM spouses WHERE id = ?", [(int) $case['spouse_id']]);
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $case['member_id']]);
// Create new independent member from spouse data
$newMemberId = $db->insert('members', [
'full_name_ar' => $spouse['full_name_ar'],
'full_name_en' => $spouse['full_name_en'] ?? null,
......@@ -293,7 +334,27 @@ class DivorceController extends Controller
$newNumber = MemberNumberGenerator::assign($newMemberId);
// Archive spouse on original member
$db->update('spouses', ['status' => 'divorced', 'is_archived' => 1, 'archived_at' => date('Y-m-d H:i:s')], '`id` = ?', [(int) $case['spouse_id']]);
$db->update('spouses', [
'status' => 'divorced',
'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $case['spouse_id']]);
// Transfer children if assigned
$childrenTransferred = 0;
if (!empty($case['children_assignment_json'])) {
$assignedChildren = json_decode($case['children_assignment_json'], true);
if (is_array($assignedChildren)) {
$childIds = array_column($assignedChildren, 'id');
foreach ($childIds as $childId) {
$db->query(
"UPDATE children SET member_id = ?, updated_at = NOW() WHERE id = ? AND member_id = ?",
[$newMemberId, (int) $childId, (int) $case['member_id']]
);
$childrenTransferred++;
}
}
}
// Update case with new membership references
$db->update('divorce_cases', [
......@@ -310,12 +371,22 @@ class DivorceController extends Controller
$db->commit();
EventBus::dispatch('divorce.completed', ['case_id' => (int) $id, 'new_member_id' => $newMemberId, 'new_number' => $newNumber]);
EventBus::dispatch('divorce.completed', [
'case_id' => (int) $id,
'new_member_id' => $newMemberId,
'new_number' => $newNumber,
'children_transferred' => $childrenTransferred,
]);
$msg = 'تم إتمام حالة الطلاق — رقم العضوية الجديد: ' . $newNumber;
if ($childrenTransferred > 0) {
$msg .= ' — تم نقل ' . $childrenTransferred . ' أبناء';
}
return $this->redirect("/divorce/{$id}")->withSuccess('تم إتمام حالة الطلاق — رقم العضوية الجديد: ' . $newNumber);
return $this->redirect("/divorce/{$id}")->withSuccess($msg);
} catch (\Throwable $e) {
$db->rollBack();
return $this->redirect("/divorce/{$id}")->withError('فشل: ' . $e->getMessage());
}
}
}
\ No newline at end of file
}
......@@ -32,9 +32,86 @@ final class DivorceFeeCalculator
return bcadd($rate, $dev, 2);
}
public static function getSuggestedPercentage(int $spouseOrder, string $caseType): array
{
if ($caseType === 'same_form') {
$data = RuleEngine::get('DIVORCE_SAME_FORM_FEE');
return [
'percentage' => $data['percentage'] ?? '10.00',
'label' => 'قُبلا في استمارة واحدة — 10%',
];
}
if ($spouseOrder <= 1) {
$data = RuleEngine::get('DIVORCE_JOINED_AFTER_FEE');
return [
'percentage' => $data['percentage'] ?? '50.00',
'label' => 'الزوج/ة الأولى — 50%',
];
}
return [
'percentage' => '75.00',
'label' => 'الزوج/ة الثانية — 75%',
];
}
public static function calculateChildFees(int $memberId, string $membershipValue): array
{
$db = App::getInstance()->db();
$children = $db->select(
"SELECT id, full_name_ar, date_of_birth, age_years FROM children WHERE member_id = ? AND is_archived = 0 AND status = 'active'",
[$memberId]
);
if (empty($children)) return ['children' => [], 'total_fee' => '0.00'];
$results = [];
$totalFee = '0.00';
foreach ($children as $child) {
$age = self::calculateAge($child['date_of_birth']);
$bracket = self::getAgeBracket($age);
$percentage = self::getChildPercentage($bracket['rule_code']);
$fee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2);
$results[] = [
'id' => (int) $child['id'],
'name' => $child['full_name_ar'],
'age' => $age,
'bracket_label' => $bracket['label'],
'percentage' => $percentage,
'fee' => $fee,
];
$totalFee = bcadd($totalFee, $fee, 2);
}
return ['children' => $results, 'total_fee' => $totalFee];
}
private static function calculateAge(string $dateOfBirth): int
{
$birth = new \DateTime($dateOfBirth);
$now = new \DateTime();
return (int) $birth->diff($now)->y;
}
private static function getAgeBracket(int $age): array
{
if ($age < 12) return ['rule_code' => 'DIVORCE_CHILD_UNDER_12', 'label' => 'أقل من 12 سنة'];
if ($age < 16) return ['rule_code' => 'DIVORCE_CHILD_12_TO_16', 'label' => '12 إلى أقل من 16 سنة'];
if ($age < 18) return ['rule_code' => 'DIVORCE_CHILD_16_TO_18', 'label' => '16 إلى أقل من 18 سنة'];
return ['rule_code' => 'DIVORCE_CHILD_OVER_18', 'label' => 'فوق 18 سنة'];
}
private static function getChildPercentage(string $ruleCode): string
{
$data = RuleEngine::get($ruleCode);
return $data['percentage'] ?? '0.00';
}
/**
* Validate eligibility and determine case type (without calculating final fee —
* the board sets the percentage).
* Validate eligibility and determine case type.
*/
public static function calculate(int $memberId, int $spouseId, string $divorceDate): array
{
......@@ -55,44 +132,50 @@ final class DivorceFeeCalculator
return ['success' => false, 'error' => "انتهت مهلة تقديم الطلب ({$maxYears} سنة من تاريخ الطلاق)"];
}
// Determine case type
$memberCreated = $member['created_at'] ?? $member['form_date'];
// Use SPOUSE's join_date for the 5-year calculation (not member's created_at)
$spouseJoinDate = $spouse['join_date'] ?? $spouse['created_at'];
$membershipValue = $member['membership_value'] ?? '0.00';
// Check children
// Check children on this membership
$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)
// Check 5-year minimum from spouse's join date (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));
$yearsMembership = (int) ((time() - strtotime($spouseJoinDate)) / (365.25 * 86400));
if ($yearsMembership < $minYears && !($waivedIfChildren && $hasChildren)) {
return ['success' => false, 'error' => "الحد الأدنى لسنوات العضوية {$minYears} سنوات (يُعفى في حالة وجود أبناء)"];
return ['success' => false, 'error' => "الحد الأدنى لسنوات العضوية {$minYears} سنوات من تاريخ انضمام الزوج/ة (يُعفى في حالة وجود أبناء)"];
}
// Determine case type
// Determine case type based on join dates
$caseType = 'joined_after';
$memberCreatedDate = substr($memberCreated, 0, 10);
$memberCreated = substr($member['created_at'] ?? $member['form_date'] ?? '', 0, 10);
$spouseJoinDateStr = substr($spouseJoinDate, 0, 10);
if ($memberCreatedDate === $spouseJoinDateStr) {
if ($memberCreated === $spouseJoinDateStr) {
$caseType = 'same_form';
}
// Spouse order determines suggested fee (50% first, 75% second)
$spouseOrder = (int) ($spouse['spouse_order'] ?? 1);
$suggested = self::getSuggestedPercentage($spouseOrder, $caseType);
return [
'success' => true,
'case_type' => $caseType,
'membership_value' => $membershipValue,
'years_of_membership' => $yearsMembership,
'has_children' => $hasChildren,
'child_count' => $childCount,
'success' => true,
'case_type' => $caseType,
'membership_value' => $membershipValue,
'years_of_membership' => $yearsMembership,
'has_children' => $hasChildren,
'child_count' => $childCount,
'spouse_order' => $spouseOrder,
'suggested_percentage' => $suggested['percentage'],
'suggested_label' => $suggested['label'],
];
}
}
\ No newline at end of file
}
This diff is collapsed.
<?php
declare(strict_types=1);
return [
'up' => "UPDATE business_rules SET current_value_json = '{\"percentage\": \"5.00\"}', updated_at = NOW() WHERE rule_code = 'DIVORCE_CHILD_UNDER_12'",
'down' => "UPDATE business_rules SET current_value_json = '{\"percentage\": \"15.00\"}', updated_at = NOW() WHERE rule_code = 'DIVORCE_CHILD_UNDER_12'",
];
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