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

fix(installments): pass pendingCount to show view; feat(transfers): فصل أبناء employment workflow

- Installments: controller now computes pendingCount before passing to show view,
  fixing Undefined variable crash at show.php:4

- Transfers: add فصل أبناء employment-status workflow
  - Migration Phase_94_002 adds is_employed, graduation_date, work_date,
    date_turned_25, effective_transfer_date columns to transfer_requests
  - SeparationFeeCalculator: new calculateYearsFloor() (floor, never rounds up),
    computeChildSeparationDates() (effective = min(work_date, date_25) or date_25),
    calculateForChildSeparation() (uses floor years + current subscription price)
  - TransferController store(): reads employment fields, computes effective date,
    routes to new calculator for child_separation; saves all new fields
  - calculateFee API: supports transfer_type=child_separation with employment fields
  - create.php: new فصل أبناء section with employment radio, graduation date,
    work date (conditional), auto-computed date_turned_25, effective date display,
    live elapsed-years preview via AJAX
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent 8b5a876b
......@@ -72,9 +72,12 @@ class InstallmentController extends Controller
[(int) $id]
);
$pendingCount = count(array_filter($schedule, fn($s) => ($s['status'] ?? '') !== 'paid'));
return $this->view('Installments.Views.show', [
'plan' => $plan,
'schedule' => $schedule,
'plan' => $plan,
'schedule' => $schedule,
'pendingCount' => $pendingCount,
]);
}
......
......@@ -89,14 +89,64 @@ class TransferController extends Controller
}
}
// Fetch child record if needed (may have been loaded in age-validation block above)
$child = isset($child) ? $child : ($childId ? $db->selectOne("SELECT * FROM children WHERE id = ?", [$childId]) : null);
// فصل أبناء: read employment fields and compute effective transfer date
$isEmployed = null;
$graduationDate = null;
$workDate = null;
$dateTurned25 = null;
$effectiveTransferDate = null;
if ($transferType === 'child_separation' && $child) {
$isEmployedPost = $request->post('is_employed', '');
if ($isEmployedPost === '') {
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب تحديد حالة التوظيف');
}
$isEmployed = (bool) (int) $isEmployedPost;
$graduationDate = trim($request->post('graduation_date', '')) ?: null;
if ($isEmployed) {
$workDate = trim($request->post('work_date', '')) ?: null;
if (!$workDate) {
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب إدخال تاريخ مباشرة العمل عند اختيار موظف');
}
}
$date25Override = trim($request->post('date_turned_25', '')) ?: null;
if (empty($child['date_of_birth'])) {
return $this->redirect("/transfers/create/{$memberId}")->withError('لا يوجد تاريخ ميلاد للابن — لا يمكن احتساب تاريخ الفصل');
}
$dates = SeparationFeeCalculator::computeChildSeparationDates(
$child['date_of_birth'],
$isEmployed,
$workDate,
$date25Override
);
$dateTurned25 = $dates['date_turned_25'];
$effectiveTransferDate = $dates['effective_transfer_date'];
}
// Calculate fees
$qualCode = null;
if ($childId) {
$child = $child ?? $db->selectOne("SELECT * FROM children WHERE id = ?", [$childId]);
// Use child's qualification if available, otherwise parent's
if ($transferType === 'child_separation' && $child && $effectiveTransferDate) {
$feeCalc = SeparationFeeCalculator::calculateForChildSeparation(
(int) $memberId,
(int) $childId,
(bool) $isEmployed,
$workDate,
$effectiveTransferDate,
$qualCode
);
} else {
$feeCalc = SeparationFeeCalculator::calculate((int) $memberId, $childId, $qualCode);
}
$feeCalc = SeparationFeeCalculator::calculate((int) $memberId, $childId, $qualCode);
if (!($feeCalc['success'] ?? false)) {
return $this->redirect("/transfers/create/{$memberId}")->withError($feeCalc['error'] ?? 'خطأ في حساب الرسوم');
}
......@@ -164,25 +214,30 @@ class TransferController extends Controller
}
$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'],
'source_companions_count' => $sourceCompanionsCount,
'target_companions_count' => $targetCompanionsCount,
'companion_surcharge' => $companionSurcharge,
'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'] ?? $feeCalc['subscription_price'] ?? '0.00',
'source_companions_count' => $sourceCompanionsCount,
'target_companions_count' => $targetCompanionsCount,
'companion_surcharge' => $companionSurcharge,
'companion_surcharge_breakdown'=> $companionBreakdown,
'years_since_acquisition' => $feeCalc['years_since_acquisition'],
'qualification_code' => $feeCalc['qualification_code'],
'fee_percentage' => $feeCalc['fee_percentage'],
'separation_fee' => $feeCalc['separation_fee'],
'form_fee' => $feeCalc['form_fee'],
'annual_subscription_fee' => $feeCalc['annual_subscription_fee'],
'total_fee' => $totalWithSurcharge,
'status' => 'requested',
'notes' => $notesJson,
'years_since_acquisition' => $feeCalc['years_since_acquisition'],
'qualification_code' => $feeCalc['qualification_code'],
'fee_percentage' => $feeCalc['fee_percentage'],
'separation_fee' => $feeCalc['separation_fee'],
'form_fee' => $feeCalc['form_fee'],
'annual_subscription_fee' => $feeCalc['annual_subscription_fee'],
'total_fee' => $totalWithSurcharge,
'status' => 'requested',
'notes' => $notesJson,
'is_employed' => $isEmployed !== null ? ($isEmployed ? 1 : 0) : null,
'graduation_date' => $graduationDate,
'work_date' => $workDate,
'date_turned_25' => $dateTurned25,
'effective_transfer_date' => $effectiveTransferDate,
]);
if (FormBridge::exists('TRANSFER_SEPARATION')) {
......@@ -350,9 +405,38 @@ class TransferController extends Controller
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;
$memberId = (int) $request->post('member_id', 0);
$childId = $request->post('child_id') ? (int) $request->post('child_id') : null;
$qualCode = $request->post('qualification_code') ?: null;
$transferType = $request->post('transfer_type', '');
if ($transferType === 'child_separation' && $childId) {
$db = App::getInstance()->db();
$child = $db->selectOne("SELECT date_of_birth FROM children WHERE id = ?", [$childId]);
$isEmployedPost = $request->post('is_employed', '');
$isEmployed = $isEmployedPost !== '' ? (bool)(int)$isEmployedPost : false;
$workDate = trim($request->post('work_date', '')) ?: null;
$date25Override = trim($request->post('date_turned_25', '')) ?: null;
if ($child && !empty($child['date_of_birth'])) {
$dates = SeparationFeeCalculator::computeChildSeparationDates(
$child['date_of_birth'],
$isEmployed,
$isEmployed ? $workDate : null,
$date25Override
);
$result = SeparationFeeCalculator::calculateForChildSeparation(
$memberId, $childId, $isEmployed,
$isEmployed ? $workDate : null,
$dates['effective_transfer_date'],
$qualCode
);
$result['date_turned_25'] = $dates['date_turned_25'];
$result['effective_transfer_date'] = $dates['effective_transfer_date'];
return $this->json($result);
}
}
$result = SeparationFeeCalculator::calculate($memberId, $childId, $qualCode);
return $this->json($result);
......
......@@ -27,6 +27,7 @@ class TransferRequest extends Model
'archive_snapshot_id', 'workflow_instance_id',
'board_decision_notes', 'approved_by', 'approved_at',
'completed_at', 'status', 'notes',
'is_employed', 'graduation_date', 'work_date', 'date_turned_25', 'effective_transfer_date',
];
public static function getForMember(int $memberId): array
......
......@@ -102,6 +102,145 @@ final class SeparationFeeCalculator
return max(1, $diff->y + ($diff->m > 0 || $diff->d > 0 ? 1 : 0)); // partial year rounds up
}
/**
* فصل أبناء specific: floors elapsed years (never rounds up partial year).
* Returns elapsed complete years between $acquisitionDate and $effectiveDate.
* Minimum 0 (not 1) — the percentage table covers year 1+.
*/
public static function calculateYearsFloor(string $acquisitionDate, string $effectiveDate): int
{
$from = new \DateTime(substr($acquisitionDate, 0, 10));
$to = new \DateTime(substr($effectiveDate, 0, 10));
$diff = $to->diff($from);
return max(1, $diff->y); // floor: only completed years; min 1 for fee-table lookup
}
/**
* Calculate the effective transfer date for فصل أبناء:
* - If employed: effective = min(work_date, date_turned_25)
* - If not employed (or work_date is null): effective = date_turned_25
*
* date_turned_25 defaults to DOB + 25 years but may be overridden by user.
*
* @param string $dobString Child's date_of_birth (YYYY-MM-DD)
* @param bool $isEmployed
* @param string|null $workDate YYYY-MM-DD, required when employed
* @param string|null $date25Override User-supplied override for date_turned_25
* @return array{date_turned_25: string, effective_transfer_date: string}
*/
public static function computeChildSeparationDates(
string $dobString,
bool $isEmployed,
?string $workDate,
?string $date25Override = null
): array {
$dob = new \DateTime(substr($dobString, 0, 10));
$dob->modify('+25 years');
$dateTurned25 = $date25Override ?? $dob->format('Y-m-d');
if ($isEmployed && $workDate) {
$effective = min($dateTurned25, $workDate);
} else {
$effective = $dateTurned25;
}
return [
'date_turned_25' => $dateTurned25,
'effective_transfer_date' => $effective,
];
}
/**
* Full فصل أبناء fee calculation.
* Uses floor years (not ceil) and effective_transfer_date as the end date.
*/
public static function calculateForChildSeparation(
int $sourceMemberId,
int $childId,
bool $isEmployed,
?string $workDate,
string $effectiveTransferDate,
?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 && !empty($member['qualification_id'])) {
$qual = $db->selectOne("SELECT code FROM qualifications WHERE id = ?", [(int) $member['qualification_id']]);
$qualificationCode = $qual['code'] ?? null;
}
// Current membership subscription price (same lookup as calculate())
$branchId = (int) ($member['branch_id'] ?? 1);
$mType = $member['membership_type'] ?? 'working';
$subscriptionPrice = '0.00';
if ($qualificationCode) {
$priceInfo = PricingEngine::getMembershipPrice($branchId, $qualificationCode);
$subscriptionPrice = $priceInfo['price'] ?? '0.00';
}
if (bccomp($subscriptionPrice, '0.01', 2) < 0) {
$qualId = !empty($member['qualification_id']) ? (int) $member['qualification_id'] : null;
if ($qualId) {
$pricing = $db->selectOne(
"SELECT price FROM pricing_configs WHERE branch_id = ? AND qualification_id = ? AND membership_type = ? AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY effective_from DESC LIMIT 1",
[$branchId, $qualId, $mType]
);
} else {
$pricing = $db->selectOne(
"SELECT price FROM pricing_configs WHERE branch_id = ? AND membership_type = ? AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY price ASC LIMIT 1",
[$branchId, $mType]
);
}
if ($pricing && bccomp($pricing['price'], '0.01', 2) >= 0) {
$subscriptionPrice = $pricing['price'];
}
}
// Elapsed years: floor from father's membership creation to effective_transfer_date
$acquisitionDate = $member['created_at'] ?? $member['form_date'] ?? date('Y-m-d');
$yearsSince = self::calculateYearsFloor($acquisitionDate, $effectiveTransferDate);
$feePercentage = self::getFeePercentageByYear($yearsSince);
// Separation fee = percentage × current subscription price
$separationFee = bcdiv(bcmul($subscriptionPrice, (string) $feePercentage, 4), '100', 2);
// Form fee + annual subscription (same as regular separation)
$formFeeData = RuleEngine::get('FORM_TRANSFER_FEE');
$formFee = $formFeeData['amount'] ?? '570.00';
$annualSub = ServicePrice::getPrice('SVC_ANNUAL_MEMBER', '492.00');
$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,
'subscription_price' => $subscriptionPrice,
'years_since_acquisition' => $yearsSince,
'fee_percentage' => $feePercentage,
'separation_fee' => $separationFee,
'form_fee' => $formFee,
'annual_subscription_fee' => $annualSubscriptionFee,
'total_fee' => $totalFee,
'acquisition_date' => $acquisitionDate,
'effective_transfer_date' => $effectiveTransferDate,
'is_employed' => $isEmployed,
'work_date' => $workDate,
];
}
public static function calculateAcquiredMemberChildFee(int $childAge, string $membershipValue): array
{
if ($childAge < 12) {
......
This diff is collapsed.
<?php
declare(strict_types=1);
return [
'up' => function (\App\Core\Database $db): void {
$cols = [
'is_employed' => "ALTER TABLE transfer_requests ADD COLUMN is_employed TINYINT(1) NULL DEFAULT NULL AFTER notes",
'graduation_date' => "ALTER TABLE transfer_requests ADD COLUMN graduation_date DATE NULL DEFAULT NULL AFTER is_employed",
'work_date' => "ALTER TABLE transfer_requests ADD COLUMN work_date DATE NULL DEFAULT NULL AFTER graduation_date",
'date_turned_25' => "ALTER TABLE transfer_requests ADD COLUMN date_turned_25 DATE NULL DEFAULT NULL AFTER work_date",
'effective_transfer_date'=> "ALTER TABLE transfer_requests ADD COLUMN effective_transfer_date DATE NULL DEFAULT NULL AFTER date_turned_25",
];
foreach ($cols as $col => $sql) {
$exists = $db->selectOne(
"SELECT 1 FROM information_schema.COLUMNS
WHERE table_schema = DATABASE() AND table_name = 'transfer_requests' AND column_name = ?",
[$col]
);
if (!$exists) {
$db->raw($sql);
}
}
},
'down' => "
ALTER TABLE transfer_requests
DROP COLUMN IF EXISTS is_employed,
DROP COLUMN IF EXISTS graduation_date,
DROP COLUMN IF EXISTS work_date,
DROP COLUMN IF EXISTS date_turned_25,
DROP COLUMN IF EXISTS effective_transfer_date;
",
];
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