Commit 2b105af3 authored by Mahmoud Aglan's avatar Mahmoud Aglan

koool

parent 1c230fd3
<?php
declare(strict_types=1);
namespace App\Modules\Members\Services;
use App\Core\App;
/**
* Handles automatic freezing of male children who reach age 25,
* subscription blocking checks, and carnet printing eligibility.
*/
final class AutoFreezeService
{
/**
* Process automatic freeze for male children aged 25+.
* Sets is_frozen = 1 with reason indicating they must convert to independent membership.
*
* @return array{frozen_count: int, processed: int}
*/
public static function processAutoFreeze(): array
{
$db = App::getInstance()->db();
$children = $db->select(
"SELECT id, date_of_birth FROM children
WHERE gender = 'male'
AND is_frozen = 0
AND is_archived = 0
AND status = 'active'"
);
$processed = count($children);
$frozenCount = 0;
$now = date('Y-m-d H:i:s');
$today = new \DateTimeImmutable('today');
foreach ($children as $child) {
$dob = new \DateTimeImmutable($child['date_of_birth']);
$age = (int) $dob->diff($today)->y;
if ($age >= 25) {
$db->update(
'children',
[
'is_frozen' => 1,
'frozen_at' => $now,
'frozen_reason' => 'بلوغ سن 25 عام - يجب التحويل لعضوية مستقلة',
],
'id = ?',
[$child['id']]
);
$frozenCount++;
}
}
return [
'frozen_count' => $frozenCount,
'processed' => $processed,
];
}
/**
* Check if a member has unpaid subscriptions for the current financial year.
* Blocks certain operations if annual subscription is not paid.
*
* @return array{blocked: bool, reason?: string, unpaid_amount?: string}
*/
public static function checkSubscriptionBlock(int $memberId): array
{
$db = App::getInstance()->db();
$financialYear = self::getCurrentFinancialYear();
$unpaid = $db->select(
"SELECT total_amount, paid_amount FROM subscriptions
WHERE member_id = ?
AND financial_year = ?
AND status IN ('pending', 'overdue')",
[$memberId, $financialYear]
);
if (!empty($unpaid)) {
$unpaidAmount = '0.00';
foreach ($unpaid as $row) {
$remaining = bcsub($row['total_amount'], $row['paid_amount'], 2);
$unpaidAmount = bcadd($unpaidAmount, $remaining, 2);
}
return [
'blocked' => true,
'reason' => 'لم يتم سداد الاشتراك السنوي',
'unpaid_amount' => $unpaidAmount,
];
}
return ['blocked' => false];
}
/**
* Check if a member is eligible to print their carnet (membership card).
* Blocked if subscription unpaid, member is suspended, or member is frozen.
*
* @return array{allowed: bool, reason?: string}
*/
public static function canPrintCarnet(int $memberId): array
{
$db = App::getInstance()->db();
// Check member status
$member = $db->selectOne(
"SELECT status FROM members WHERE id = ? AND is_archived = 0",
[$memberId]
);
if (!$member) {
return [
'allowed' => false,
'reason' => 'العضو غير موجود',
];
}
if ($member['status'] === 'suspended') {
return [
'allowed' => false,
'reason' => 'العضوية موقوفة',
];
}
if ($member['status'] === 'frozen') {
return [
'allowed' => false,
'reason' => 'العضوية مجمدة',
];
}
// Check subscription block
$subscriptionCheck = self::checkSubscriptionBlock($memberId);
if ($subscriptionCheck['blocked']) {
return [
'allowed' => false,
'reason' => $subscriptionCheck['reason'],
];
}
return ['allowed' => true];
}
/**
* Get current financial year string.
* FY runs July to June: if month >= 7, FY is "thisYear/thisYear+1", else "lastYear/thisYear".
*/
private static function getCurrentFinancialYear(): string
{
$month = (int) date('n');
$year = (int) date('Y');
if ($month >= 7) {
return $year . '/' . ($year + 1);
}
return ($year - 1) . '/' . $year;
}
}
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\Members\Services;
use App\Core\App;
use App\Core\Logger;
/**
* Handles membership waiver (التنازل عن العضوية).
* A waiver transfers the membership from one person to another with board approval.
*/
final class WaiverService
{
private const WAIVER_FEE_PERCENTAGE = '30.00';
/**
* Calculate the waiver fee for a member based on their current membership value.
* Fee = 30% of current membership value.
*
* @return array{membership_value: string, percentage: string, waiver_fee: string, annual_subscription_required: bool}
*/
public static function calculateWaiverFee(int $memberId): array
{
$db = App::getInstance()->db();
$member = $db->selectOne(
"SELECT id, membership_value, qualification_id FROM members WHERE id = ? AND is_archived = 0",
[$memberId]
);
if (!$member) {
return [
'membership_value' => '0.00',
'percentage' => self::WAIVER_FEE_PERCENTAGE,
'waiver_fee' => '0.00',
'annual_subscription_required' => true,
];
}
$membershipValue = $member['membership_value'] ?? '0.00';
// If membership_value is zero or not set, try service_catalog based on qualification
if (bccomp($membershipValue, '0', 2) <= 0 && !empty($member['qualification_id'])) {
$qual = $db->selectOne(
"SELECT code FROM qualifications WHERE id = ?",
[(int) $member['qualification_id']]
);
if ($qual && !empty($qual['code'])) {
$serviceCode = 'SVC_MEMBERSHIP_' . strtoupper($qual['code']);
$catalogValue = \App\Modules\ServiceCatalog\Models\ServicePrice::getPrice($serviceCode);
if ($catalogValue !== null) {
$membershipValue = $catalogValue;
}
}
}
$waiverFee = bcdiv(bcmul($membershipValue, self::WAIVER_FEE_PERCENTAGE, 4), '100', 2);
return [
'membership_value' => $membershipValue,
'percentage' => self::WAIVER_FEE_PERCENTAGE,
'waiver_fee' => $waiverFee,
'annual_subscription_required' => true,
];
}
/**
* Get the list of conditions required for a membership waiver.
*
* @return array<int, string>
*/
public static function getWaiverConditions(): array
{
return [
'موافقة مجلس الإدارة مطلوبة',
'لا يجوز أن يتجاوز عدد المرافقين للمتنازل إليه عدد مرافقي المتنازل',
'يحتفظ بنفس رقم العضوية',
'يجب سداد الاشتراك السنوي قبل التنازل',
'يتم أرشفة بيانات العضو القديم (لا تُحذف)',
];
}
/**
* Process a waiver request: validate source member, count dependants,
* and create a transfer_request record.
*
* @param int $sourceMemberId ID of the member transferring their membership
* @param array $newMemberData Data for the new member receiving the membership
*
* @return array{transfer_request_id: int, fee: string, conditions: array<int, string>}
*
* @throws \RuntimeException If source member is not active or not found
*/
public static function processWaiver(int $sourceMemberId, array $newMemberData): array
{
$db = App::getInstance()->db();
// Validate source member exists and is active
$sourceMember = $db->selectOne(
"SELECT id, status, membership_number, membership_value FROM members WHERE id = ? AND is_archived = 0",
[$sourceMemberId]
);
if (!$sourceMember) {
throw new \RuntimeException('العضو المصدر غير موجود');
}
if ($sourceMember['status'] !== 'active') {
throw new \RuntimeException('العضو المصدر غير نشط - لا يمكن التنازل');
}
// Count dependants (spouses + children) on source
$spouseCount = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM spouses WHERE member_id = ? AND is_archived = 0",
[$sourceMemberId]
)['cnt'] ?? 0);
$childCount = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM children WHERE member_id = ? AND is_archived = 0",
[$sourceMemberId]
)['cnt'] ?? 0);
$totalDependants = $spouseCount + $childCount;
// Calculate fee
$feeData = self::calculateWaiverFee($sourceMemberId);
// Create transfer_request record
$now = date('Y-m-d H:i:s');
$db->insert('transfer_requests', [
'source_member_id' => $sourceMemberId,
'transfer_type' => 'waiver',
'source_membership_number' => $sourceMember['membership_number'],
'new_membership_value' => $sourceMember['membership_value'] ?? '0.00',
'fee_percentage' => self::WAIVER_FEE_PERCENTAGE,
'total_fee' => $feeData['waiver_fee'],
'status' => 'requested',
'notes' => json_encode([
'new_member_data' => $newMemberData,
'source_dependants' => $totalDependants,
'spouse_count' => $spouseCount,
'child_count' => $childCount,
], JSON_UNESCAPED_UNICODE),
'created_at' => $now,
'updated_at' => $now,
]);
$transferRequestId = (int) $db->selectOne("SELECT LAST_INSERT_ID() as id")['id'];
Logger::info('Waiver request created', [
'transfer_request_id' => $transferRequestId,
'source_member_id' => $sourceMemberId,
'fee' => $feeData['waiver_fee'],
'dependants' => $totalDependants,
]);
return [
'transfer_request_id' => $transferRequestId,
'fee' => $feeData['waiver_fee'],
'conditions' => self::getWaiverConditions(),
];
}
}
...@@ -16,14 +16,27 @@ final class SubscriptionGenerator ...@@ -16,14 +16,27 @@ final class SubscriptionGenerator
$ts = date('Y-m-d H:i:s'); $ts = date('Y-m-d H:i:s');
$empId = $employee ? (int) $employee->id : null; $empId = $employee ? (int) $employee->id : null;
// Get rates from service catalog // Get year-specific rates (e.g. 2024/2025 -> suffix 2024)
$memberRate = self::getRate('SVC_ANNUAL_MEMBER'); $yearParts = explode('/', $financialYear);
$spouseRate = self::getRate('SVC_ANNUAL_SPOUSE'); $yearSuffix = $yearParts[0] ?? '';
$childRate = self::getRate('SVC_ANNUAL_CHILD'); $memberRate = self::getRate('SVC_ANNUAL_MEMBER_' . $yearSuffix) ?: self::getRate('SVC_ANNUAL_MEMBER');
$tempRate = self::getRate('SVC_ANNUAL_TEMP'); $spouseRate = self::getRate('SVC_ANNUAL_SPOUSE_' . $yearSuffix) ?: self::getRate('SVC_ANNUAL_SPOUSE');
$childRate = self::getRate('SVC_ANNUAL_CHILD_' . $yearSuffix) ?: self::getRate('SVC_ANNUAL_CHILD');
$tempRate = self::getRate('SVC_ANNUAL_TEMP_' . $yearSuffix) ?: self::getRate('SVC_ANNUAL_TEMP');
$devFeeData = RuleEngine::get('DEVELOPMENT_FEE'); $devFeeData = RuleEngine::get('DEVELOPMENT_FEE');
$devFee = $devFeeData['amount'] ?? '35.00'; $devFee = $devFeeData['amount'] ?? '35.00';
// Apply year-specific discount/increase from rules
$yearAdjustment = RuleEngine::get('SUBSCRIPTION_YEAR_ADJUSTMENT_' . $yearSuffix);
if ($yearAdjustment && isset($yearAdjustment['discount_percentage'])) {
$discPct = $yearAdjustment['discount_percentage'];
$multiplier = bcsub('1.00', bcdiv($discPct, '100', 4), 4);
$memberRate = bcmul($memberRate, $multiplier, 2);
$spouseRate = bcmul($spouseRate, $multiplier, 2);
$childRate = bcmul($childRate, $multiplier, 2);
$tempRate = bcmul($tempRate, $multiplier, 2);
}
// Get active members // Get active members
$memberWhere = "m.status = 'active' AND m.is_archived = 0 AND m.membership_type NOT IN ('honorary')"; $memberWhere = "m.status = 'active' AND m.is_archived = 0 AND m.membership_type NOT IN ('honorary')";
$memberParams = []; $memberParams = [];
......
...@@ -78,6 +78,31 @@ final class SeparationFeeCalculator ...@@ -78,6 +78,31 @@ final class SeparationFeeCalculator
return max(1, $diff->y + ($diff->m > 0 || $diff->d > 0 ? 1 : 0)); // partial year rounds up return max(1, $diff->y + ($diff->m > 0 || $diff->d > 0 ? 1 : 0)); // partial year rounds up
} }
public static function calculateAcquiredMemberChildFee(int $childAge, string $membershipValue): array
{
if ($childAge < 12) {
$ruleCode = 'DIVORCE_CHILD_UNDER_12';
} elseif ($childAge < 16) {
$ruleCode = 'DIVORCE_CHILD_12_TO_16';
} elseif ($childAge < 18) {
$ruleCode = 'DIVORCE_CHILD_16_TO_18';
} else {
$ruleCode = 'DIVORCE_CHILD_OVER_18';
}
$data = RuleEngine::get($ruleCode);
$percentage = $data['percentage'] ?? '30.00';
$fee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2);
return [
'age' => $childAge,
'rule_code' => $ruleCode,
'percentage' => $percentage,
'fee' => $fee,
'membership_value' => $membershipValue,
];
}
public static function getFeePercentageByYear(int $year): string public static function getFeePercentageByYear(int $year): string
{ {
$ruleMap = [ $ruleMap = [
......
<?php
declare(strict_types=1);
namespace App\Modules\Transfers\Services;
use App\Core\App;
use App\Modules\Rules\Services\RuleEngine;
final class TransferEligibility
{
public static function canChildSeparate(int $childId): array
{
$db = App::getInstance()->db();
$child = $db->selectOne("SELECT * FROM children WHERE id = ? AND is_archived = 0", [$childId]);
if (!$child) {
return ['eligible' => false, 'reason' => 'لم يتم العثور على بيانات الابن/الابنة'];
}
$age = self::calculateAge($child['date_of_birth']);
$gender = $child['gender'];
if ($gender === 'female') {
return ['eligible' => true, 'reason' => 'البنات: يتم الفصل عند الزواج', 'condition' => 'marriage'];
}
$maxAge = 25;
$ruleData = RuleEngine::get('MALE_CHILD_FREEZE_AGE');
if ($ruleData) {
$maxAge = (int) ($ruleData['value'] ?? 25);
}
if ($age >= $maxAge) {
return ['eligible' => true, 'reason' => 'بلوغ سن ' . $maxAge . ' عام', 'condition' => 'age'];
}
return [
'eligible' => true,
'reason' => 'الأبناء: التخرج من الجامعة ويعمل بأجر أو بلوغ 25 عام إيهما اسبق',
'condition' => 'graduation_or_age',
'requires_verification' => true,
];
}
public static function canSpouseSeparateDivorce(int $spouseId, ?string $divorceDate = null): array
{
$db = App::getInstance()->db();
$spouse = $db->selectOne("SELECT * FROM spouses WHERE id = ? AND is_archived = 0", [$spouseId]);
if (!$spouse) {
return ['eligible' => false, 'reason' => 'لم يتم العثور على بيانات الزوج/الزوجة'];
}
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $spouse['member_id']]);
if (!$member) {
return ['eligible' => false, 'reason' => 'العضو الأساسي غير موجود'];
}
$divorceWindowData = RuleEngine::get('DIVORCE_REQUEST_WINDOW');
$maxYears = (int) ($divorceWindowData['max_years'] ?? 1);
if ($divorceDate) {
$divorceDateTime = new \DateTime($divorceDate);
$now = new \DateTime();
$diff = $now->diff($divorceDateTime);
if ($diff->y >= $maxYears) {
return ['eligible' => false, 'reason' => 'مر أكثر من ' . $maxYears . ' عام على تاريخ الطلاق'];
}
}
$minYearsData = RuleEngine::get('DIVORCE_MIN_MEMBERSHIP_YEARS');
$minYears = (int) ($minYearsData['min_years'] ?? 5);
$waivedIfChildren = (bool) ($minYearsData['waived_if_children'] ?? true);
$membershipDate = $spouse['join_date'] ?? $spouse['marriage_date'] ?? $member['created_at'];
$membershipYears = self::calculateAge($membershipDate);
if ($membershipYears < $minYears) {
if ($waivedIfChildren) {
$childCount = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM children WHERE member_id = ? AND is_archived = 0",
[(int) $spouse['member_id']]
)['cnt'] ?? 0);
if ($childCount === 0) {
return [
'eligible' => false,
'reason' => 'لم تمر ' . $minYears . ' سنوات على اكتساب العضوية ولا يوجد أبناء',
];
}
} else {
return [
'eligible' => false,
'reason' => 'لم تمر ' . $minYears . ' سنوات على اكتساب العضوية',
];
}
}
return ['eligible' => true, 'reason' => 'مستوفي شروط التحويل'];
}
public static function getDivorceTransferFee(int $spouseId, string $membershipValue): array
{
$db = App::getInstance()->db();
$spouse = $db->selectOne("SELECT * FROM spouses WHERE id = ?", [$spouseId]);
if (!$spouse) {
return ['error' => 'الزوج/الزوجة غير موجود'];
}
$classification = $spouse['classification'] ?? 'working';
if ($classification === 'working') {
return [
'scenario' => 'both_working',
'fee_type' => 'annual_subscription_only',
'percentage' => '0.00',
'separation_fee' => '0.00',
'description' => 'زوج/زوجة أعضاء عاملين — الاشتراك السنوي فقط',
];
}
if ($classification === 'initial_form' || $spouse['spouse_order'] == 1) {
$data = RuleEngine::get('DIVORCE_SAME_FORM_FEE');
$pct = $data['percentage'] ?? '10.00';
$fee = bcdiv(bcmul($membershipValue, $pct, 4), '100', 2);
return [
'scenario' => 'same_form',
'fee_type' => 'percentage',
'percentage' => $pct,
'separation_fee' => $fee,
'treat_as' => 'membership_basis',
'description' => 'تم قبولهم في استمارة العضوية — 10% من قيمة العضوية',
];
}
$data = RuleEngine::get('DIVORCE_JOINED_AFTER_FEE');
$pct = $data['percentage'] ?? '50.00';
$fee = bcdiv(bcmul($membershipValue, $pct, 4), '100', 2);
return [
'scenario' => 'joined_after',
'fee_type' => 'percentage',
'percentage' => $pct,
'separation_fee' => $fee,
'treat_as' => 'acquired_member',
'description' => 'تم الانضمام بعد الحصول على العضوية — 50% من قيمة العضوية',
];
}
public static function canWaiver(int $memberId): array
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) {
return ['eligible' => false, 'reason' => 'العضو غير موجود'];
}
if ($member['status'] !== 'active') {
return ['eligible' => false, 'reason' => 'العضوية غير فعالة'];
}
$unpaidSubs = $db->selectOne(
"SELECT COUNT(*) as cnt FROM subscriptions WHERE member_id = ? AND status IN ('pending','overdue')",
[$memberId]
);
if ((int) ($unpaidSubs['cnt'] ?? 0) > 0) {
return ['eligible' => false, 'reason' => 'يجب سداد جميع الاشتراكات المتأخرة قبل التنازل'];
}
return [
'eligible' => true,
'reason' => 'مستوفي شروط التنازل — يتطلب موافقة مجلس الأمناء',
'requires_board_approval' => true,
];
}
private static function calculateAge(string $dateOfBirth): int
{
$dob = new \DateTime(substr($dateOfBirth, 0, 10));
$now = new \DateTime();
return $now->diff($dob)->y;
}
}
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