Commit 75815e05 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fixed shit

parent 0af2ed0b
...@@ -7,12 +7,10 @@ use App\Core\App; ...@@ -7,12 +7,10 @@ use App\Core\App;
use App\Modules\Children\Models\Child; use App\Modules\Children\Models\Child;
use App\Modules\Pricing\Services\PricingEngine; use App\Modules\Pricing\Services\PricingEngine;
use App\Modules\Rules\Services\RuleEngine; use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Members\Services\FormFeeService;
final class ChildFeeCalculator final class ChildFeeCalculator
{ {
/**
* Calculate the fee and classification for adding a child.
*/
public static function calculate(int $memberId, array $childData): array public static function calculate(int $memberId, array $childData): array
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
...@@ -27,7 +25,6 @@ final class ChildFeeCalculator ...@@ -27,7 +25,6 @@ final class ChildFeeCalculator
return ['error' => 'قيمة العضوية غير محددة', 'fee' => '0.00', 'classification' => 'included']; return ['error' => 'قيمة العضوية غير محددة', 'fee' => '0.00', 'classification' => 'included'];
} }
// Get child age
$childDob = $childData['date_of_birth'] ?? null; $childDob = $childData['date_of_birth'] ?? null;
$childAge = 0; $childAge = 0;
if ($childDob) { if ($childDob) {
...@@ -35,22 +32,11 @@ final class ChildFeeCalculator ...@@ -35,22 +32,11 @@ final class ChildFeeCalculator
$childAge = $age['years']; $childAge = $age['years'];
} }
// Count existing children under 18 to determine order
$childrenUnder18 = Child::countActiveUnder18ForMember($memberId);
$totalChildren = Child::countActiveForMember($memberId); $totalChildren = Child::countActiveForMember($memberId);
$childOrder = $totalChildren + 1; $childOrder = $totalChildren + 1;
// Use PricingEngine
$feeResult = PricingEngine::calculateChildFee($membershipValue, $childAge, $childOrder); $feeResult = PricingEngine::calculateChildFee($membershipValue, $childAge, $childOrder);
// Add form fee if post-creation
$formFee = '0.00';
if ($member['status'] !== 'potential') {
$formFeeData = RuleEngine::get('FORM_ADDITION_FEE');
$formFee = $formFeeData['amount'] ?? '570.00';
}
// Determine classification
$classification = $feeResult['classification'] ?? 'included'; $classification = $feeResult['classification'] ?? 'included';
if ($classification === 'not_accepted') { if ($classification === 'not_accepted') {
return [ return [
...@@ -69,18 +55,20 @@ final class ChildFeeCalculator ...@@ -69,18 +55,20 @@ final class ChildFeeCalculator
$childFee = $feeResult['fee'] ?? '0.00'; $childFee = $feeResult['fee'] ?? '0.00';
$formFee = FormFeeService::getFormFee($memberId, $member);
return [ return [
'child_order' => $childOrder, 'child_order' => $childOrder,
'child_age' => $childAge, 'child_age' => $childAge,
'children_under_18' => $childrenUnder18, 'children_under_18' => Child::countActiveUnder18ForMember($memberId),
'membership_value' => $membershipValue, 'membership_value' => $membershipValue,
'classification' => $classification, 'classification' => $classification,
'fee' => $childFee, 'fee' => $childFee,
'percentage' => $feeResult['percentage'] ?? '0.00', 'percentage' => $feeResult['percentage'] ?? '0.00',
'form_fee' => $formFee, 'form_fee' => $formFee,
'total_fee' => bcadd($childFee, $formFee, 2), 'total_fee' => bcadd($childFee, $formFee, 2),
'rule_applied' => $feeResult['rule_applied'] ?? '', 'rule_applied' => $feeResult['rule_applied'] ?? '',
'error' => null, 'error' => null,
]; ];
} }
} }
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Members\Services;
use App\Core\App;
use App\Modules\Rules\Services\RuleEngine;
use App\Modules\ServiceCatalog\Models\ServicePrice;
/**
* Determines whether a dependant addition needs a form fee (570 EGP).
*
* Business rule:
* - During initial membership creation (member status = potential / under_review),
* all additions are on the same initial form — no additional form fee.
* - After activation, any modification needs a "form addition" (570 EGP).
* - Multiple additions within the SAME form session share one form fee.
* A form session is "open" if a form fee was paid (or is pending) within
* the configured validity window (default 30 days).
*/
final class FormFeeService
{
/**
* Check if the member is still on the initial membership form.
*/
public static function isOnInitialForm(array $member): bool
{
return in_array($member['status'] ?? '', ['potential', 'under_review'], true);
}
/**
* Check if there's an active (open) addition form session for this member.
*
* An addition form is "open" if any addition_fee payment request was created
* within the configured validity window. The form fee is embedded in the first
* addition's total — subsequent additions within the same window share it.
*/
public static function hasOpenAdditionForm(int $memberId): bool
{
$db = App::getInstance()->db();
$validityDays = (int) (RuleEngine::getValue('ADDITION_FORM_VALIDITY_DAYS', 'value') ?? 30);
$cutoff = date('Y-m-d H:i:s', strtotime("-{$validityDays} days"));
$existing = $db->selectOne(
"SELECT id FROM payment_requests
WHERE member_id = ? AND payment_type = 'addition_fee' AND is_voided = 0
AND status IN ('pending', 'processing', 'completed')
AND created_at >= ?
LIMIT 1",
[$memberId, $cutoff]
);
if ($existing) return true;
$paid = $db->selectOne(
"SELECT id FROM payments
WHERE member_id = ? AND payment_type = 'addition_fee' AND is_voided = 0
AND created_at >= ?
LIMIT 1",
[$memberId, $cutoff]
);
return $paid !== null;
}
/**
* Get the form fee that should be charged for an addition.
* Returns '0.00' if on initial form or if an addition form is already open.
*/
public static function getFormFee(int $memberId, array $member): string
{
if (self::isOnInitialForm($member)) {
return '0.00';
}
if (self::hasOpenAdditionForm($memberId)) {
return '0.00';
}
$feeData = RuleEngine::get('FORM_ADDITION_FEE');
return $feeData['amount'] ?? ServicePrice::getPrice('SVC_ADDITION_FORM', '570.00');
}
/**
* Check if this is a "free" spouse slot on the initial form.
* Rule: first N spouses are free (default N=1) during initial creation.
*/
public static function isSpouseFreeOnInitialForm(int $memberId, int $spouseOrder, array $member): bool
{
if (!self::isOnInitialForm($member)) {
return false;
}
$maxFree = (int) (RuleEngine::getValue('INITIAL_FREE_SPOUSES_COUNT', 'value') ?? 1);
return $spouseOrder <= $maxFree;
}
/**
* Check if this child is in a "free" slot on the initial form.
* Rule: first N children under max age are free (default N=2, age<18).
* Uses INITIAL_FREE_CHILDREN_COUNT for the initial form specifically,
* and CHILD_INCLUDED_MAX_COUNT / CHILD_INCLUDED_MAX_AGE for general classification.
*/
public static function isChildFreeOnInitialForm(int $memberId, int $childOrder, int $childAge, array $member): bool
{
if (!self::isOnInitialForm($member)) {
return false;
}
$maxFreeCount = (int) (RuleEngine::getValue('INITIAL_FREE_CHILDREN_COUNT', 'value') ?? 2);
$maxFreeAge = (int) (RuleEngine::getValue('CHILD_INCLUDED_MAX_AGE', 'value') ?? 18);
return $childOrder <= $maxFreeCount && $childAge < $maxFreeAge;
}
}
...@@ -174,7 +174,7 @@ final class MemberNumberGenerator ...@@ -174,7 +174,7 @@ final class MemberNumberGenerator
try { try {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$fee = $db->selectOne( $fee = $db->selectOne(
"SELECT base_amount FROM service_catalog WHERE service_code = 'FORM_NEW_MEMBERSHIP' AND is_active = 1 LIMIT 1" "SELECT base_amount FROM service_catalog WHERE service_code IN ('SVC_NEW_FORM','FORM_NEW_MEMBERSHIP') AND is_active = 1 ORDER BY service_code ASC LIMIT 1"
); );
if ($fee && $fee['base_amount']) { if ($fee && $fee['base_amount']) {
return $fee['base_amount']; return $fee['base_amount'];
......
...@@ -5,24 +5,21 @@ namespace App\Modules\Spouses\Services; ...@@ -5,24 +5,21 @@ namespace App\Modules\Spouses\Services;
use App\Core\App; use App\Core\App;
use App\Modules\Spouses\Models\Spouse; use App\Modules\Spouses\Models\Spouse;
use App\Modules\ServiceCatalog\Models\ServicePrice; use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Members\Services\FormFeeService;
/** /**
* Spouse Fee Calculator * Spouse Fee Calculator — all percentages/amounts read from RuleEngine.
* *
* RULES FROM CLUB REGULATIONS: * RULES (editable via /rules admin):
* ═══════════════════════════════════════════════════════════════ * 1st Spouse (initial form, not foreign): FREE (INITIAL_FREE_SPOUSES_COUNT)
* 1st Spouse (same form / initial creation): FREE (included in membership value) * 1st Spouse (late, basic member): SPOUSE_BASE_MEMBER_FEE %
* 1st Spouse (added later, أساسي basis): 15% of membership value + 570 form * 1st Spouse (late, acquired member): SPOUSE_ACQUIRED_MEMBER_FEE %
* 1st Spouse (added later, مكتسب acquired): 50% of membership value + 570 form * Foreign Spouse: SPOUSE_FOREIGN_FEE %
* Foreign Spouse (1st): 15% of membership value * 2nd Spouse: SPOUSE_2ND_FEE (percentage + annual_flat × years)
* 2nd Spouse: 10% of membership value + 150 EGP × years * 3rd Spouse: SPOUSE_3RD_FEE (percentage + annual_flat × years)
* 3rd Spouse: 20% of membership value + 200 EGP × years * 4th+ Spouse: SPOUSE_4TH_FEE (percentage + annual_flat × years)
* 4th Spouse: 30% of membership value + 300 EGP × years * Late addition form fee: FORM_ADDITION_FEE (shared via FormFeeService)
* Years = from marriage date or membership acquisition, whichever LATER
* Partial year = full year (كسر السنة سنة كاملة)
* Late addition = +570 EGP form fee
* ═══════════════════════════════════════════════════════════════
*/ */
final class SpouseFeeCalculator final class SpouseFeeCalculator
{ {
...@@ -37,39 +34,26 @@ final class SpouseFeeCalculator ...@@ -37,39 +34,26 @@ final class SpouseFeeCalculator
$membershipValue = $member['membership_value'] ?? '0.00'; $membershipValue = $member['membership_value'] ?? '0.00';
// ── HARD BLOCK: No membership value = no spouse ──
if (bccomp($membershipValue, '0.01', 2) < 0) { if (bccomp($membershipValue, '0.01', 2) < 0) {
return self::error('يجب تحديد قيمة العضوية أولاً (ملء الاستمارة واختيار المؤهل)'); return self::error('يجب تحديد قيمة العضوية أولاً (ملء الاستمارة واختيار المؤهل)');
} }
// ── Determine spouse order ──
$existingCount = Spouse::countActiveForMember($memberId); $existingCount = Spouse::countActiveForMember($memberId);
$spouseOrder = $existingCount + 1; $spouseOrder = $existingCount + 1;
// ── Is this during initial creation or later? ── $isInitialCreation = FormFeeService::isOnInitialForm($member);
// "initial" = member status is still potential AND no membership_fee/down_payment paid yet $isFirstFreeSlot = FormFeeService::isSpouseFreeOnInitialForm($memberId, $spouseOrder, $member);
$isInitialCreation = in_array($member['status'] ?? '', ['potential', 'under_review']);
// Even during initial creation, only the FIRST spouse is free
// 2nd/3rd/4th ALWAYS have fees
$isFirstSpouseDuringCreation = ($spouseOrder === 1 && $isInitialCreation);
// Late addition = member is already active/accepted = must pay form fee $formFee = FormFeeService::getFormFee($memberId, $member);
$isLateAddition = !in_array($member['status'] ?? '', ['potential', 'under_review']);
$formFee = $isLateAddition ? ServicePrice::getPrice('SVC_ADDITION_FORM', '570.00') : '0.00';
// ── Is the member acquired (مكتسب)? ──
$isAcquiredMember = self::isAcquiredMember($memberId); $isAcquiredMember = self::isAcquiredMember($memberId);
// ── Nationality check ──
$nationality = trim($spouseData['nationality'] ?? 'مصري'); $nationality = trim($spouseData['nationality'] ?? 'مصري');
$isForeign = ($nationality !== 'مصري' && $nationality !== '' && $nationality !== 'Egyptian'); $isForeign = ($nationality !== 'مصري' && $nationality !== '' && $nationality !== 'Egyptian');
// ── Marriage date for per-year calculation ──
$marriageDate = $spouseData['marriage_date'] ?? null; $marriageDate = $spouseData['marriage_date'] ?? null;
$memberCreatedDate = substr($member['created_at'] ?? date('Y-m-d'), 0, 10); $memberCreatedDate = substr($member['created_at'] ?? date('Y-m-d'), 0, 10);
// ── Calculate based on spouse order ──
$percentage = '0.00'; $percentage = '0.00';
$percentageFee = '0.00'; $percentageFee = '0.00';
$annualPerYear = '0.00'; $annualPerYear = '0.00';
...@@ -78,65 +62,63 @@ final class SpouseFeeCalculator ...@@ -78,65 +62,63 @@ final class SpouseFeeCalculator
$ruleApplied = ''; $ruleApplied = '';
switch (true) { switch (true) {
// ═══ 1ST SPOUSE ═══
case ($spouseOrder === 1): case ($spouseOrder === 1):
if ($isFirstSpouseDuringCreation && !$isForeign) { if ($isFirstFreeSlot && !$isForeign) {
// Included in base price — truly free
$percentage = '0.00'; $percentage = '0.00';
$ruleApplied = 'الزوجة الأولى — مشمولة في قيمة العضوية الأساسية (بدون رسوم إضافية)'; $ruleApplied = 'الزوجة الأولى — مشمولة في قيمة العضوية الأساسية (بدون رسوم إضافية)';
} elseif ($isForeign) { } elseif ($isForeign) {
$percentage = '15.00'; $data = RuleEngine::get('SPOUSE_FOREIGN_FEE');
$ruleApplied = 'زوج/ة أجنبي — 15% من قيمة العضوية'; $percentage = $data['percentage'] ?? '15.00';
$ruleApplied = 'زوج/ة أجنبي — ' . $percentage . '% من قيمة العضوية';
} elseif ($isAcquiredMember) { } elseif ($isAcquiredMember) {
$percentage = '50.00'; $data = RuleEngine::get('SPOUSE_ACQUIRED_MEMBER_FEE');
$ruleApplied = 'إضافة زوج/ة لعضو مكتسب العضوية (فصل/طلاق/وفاة/تنازل) — 50% من قيمة العضوية'; $percentage = $data['percentage'] ?? '50.00';
$ruleApplied = 'إضافة زوج/ة لعضو مكتسب العضوية (فصل/طلاق/وفاة/تنازل) — ' . $percentage . '% من قيمة العضوية';
} else { } else {
$percentage = '15.00'; $data = RuleEngine::get('SPOUSE_BASE_MEMBER_FEE');
$ruleApplied = 'إضافة زوج/ة لعضو أساس العضوية (إضافة لاحقة) — 15% من قيمة العضوية'; $percentage = $data['percentage'] ?? '15.00';
$ruleApplied = 'إضافة زوج/ة لعضو أساس العضوية (إضافة لاحقة) — ' . $percentage . '% من قيمة العضوية';
} }
$percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2); $percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2);
break; break;
// ═══ 2ND SPOUSE ═══
case ($spouseOrder === 2): case ($spouseOrder === 2):
$percentage = '10.00'; $data = RuleEngine::get('SPOUSE_2ND_FEE');
$annualPerYear = '150.00'; $percentage = $data['percentage'] ?? '10.00';
$ruleApplied = 'الزوجة الثانية — 10% من قيمة العضوية + 150 ج.م عن كل سنة'; $annualPerYear = $data['annual_flat'] ?? '150.00';
$ruleApplied = 'الزوجة الثانية — ' . $percentage . '% من قيمة العضوية + ' . $annualPerYear . ' ج.م عن كل سنة';
$percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2); $percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2);
$yearCount = self::calculateYears($marriageDate, $memberCreatedDate); $yearCount = self::calculateYears($marriageDate, $memberCreatedDate);
$yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2); $yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2);
break; break;
// ═══ 3RD SPOUSE ═══
case ($spouseOrder === 3): case ($spouseOrder === 3):
$percentage = '20.00'; $data = RuleEngine::get('SPOUSE_3RD_FEE');
$annualPerYear = '200.00'; $percentage = $data['percentage'] ?? '20.00';
$ruleApplied = 'الزوجة الثالثة — 20% من قيمة العضوية + 200 ج.م عن كل سنة'; $annualPerYear = $data['annual_flat'] ?? '200.00';
$ruleApplied = 'الزوجة الثالثة — ' . $percentage . '% من قيمة العضوية + ' . $annualPerYear . ' ج.م عن كل سنة';
$percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2); $percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2);
$yearCount = self::calculateYears($marriageDate, $memberCreatedDate); $yearCount = self::calculateYears($marriageDate, $memberCreatedDate);
$yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2); $yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2);
break; break;
// ═══ 4TH SPOUSE ═══
case ($spouseOrder >= 4): case ($spouseOrder >= 4):
$percentage = '30.00'; $data = RuleEngine::get('SPOUSE_4TH_FEE');
$annualPerYear = '300.00'; $percentage = $data['percentage'] ?? '30.00';
$ruleApplied = 'الزوجة الرابعة — 30% من قيمة العضوية + 300 ج.م عن كل سنة'; $annualPerYear = $data['annual_flat'] ?? '300.00';
$ruleApplied = 'الزوجة الرابعة — ' . $percentage . '% من قيمة العضوية + ' . $annualPerYear . ' ج.م عن كل سنة';
$percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2); $percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2);
$yearCount = self::calculateYears($marriageDate, $memberCreatedDate); $yearCount = self::calculateYears($marriageDate, $memberCreatedDate);
$yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2); $yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2);
break; break;
} }
// ── Total ──
$additionFee = bcadd($percentageFee, $yearlyTotal, 2); $additionFee = bcadd($percentageFee, $yearlyTotal, 2);
$totalFee = bcadd($additionFee, $formFee, 2); $totalFee = bcadd($additionFee, $formFee, 2);
// ── SANITY CHECK: 2nd+ spouse must NEVER be free ──
if ($spouseOrder >= 2 && bccomp($additionFee, '0.01', 2) < 0) { if ($spouseOrder >= 2 && bccomp($additionFee, '0.01', 2) < 0) {
// This should never happen if membership_value > 0
return self::error( return self::error(
'خطأ في حساب الرسوم — الزوجة رقم ' . $spouseOrder . 'خطأ في حساب الرسوم — الزوجة رقم ' . $spouseOrder .
' يجب أن تكون برسوم. قيمة العضوية: ' . money($membershipValue) ' يجب أن تكون برسوم. قيمة العضوية: ' . money($membershipValue)
); );
} }
...@@ -145,7 +127,7 @@ final class SpouseFeeCalculator ...@@ -145,7 +127,7 @@ final class SpouseFeeCalculator
'spouse_order' => $spouseOrder, 'spouse_order' => $spouseOrder,
'membership_value' => $membershipValue, 'membership_value' => $membershipValue,
'is_initial' => $isInitialCreation, 'is_initial' => $isInitialCreation,
'is_late_addition' => $isLateAddition, 'is_late_addition' => !$isInitialCreation,
'is_acquired' => $isAcquiredMember, 'is_acquired' => $isAcquiredMember,
'is_foreign' => $isForeign, 'is_foreign' => $isForeign,
'percentage' => $percentage, 'percentage' => $percentage,
...@@ -161,15 +143,11 @@ final class SpouseFeeCalculator ...@@ -161,15 +143,11 @@ final class SpouseFeeCalculator
'breakdown' => self::buildBreakdown( 'breakdown' => self::buildBreakdown(
$spouseOrder, $membershipValue, $percentage, $percentageFee, $spouseOrder, $membershipValue, $percentage, $percentageFee,
$annualPerYear, $yearCount, $yearlyTotal, $annualPerYear, $yearCount, $yearlyTotal,
$formFee, $totalFee, $ruleApplied, $isFirstSpouseDuringCreation $formFee, $totalFee, $ruleApplied, $isFirstFreeSlot && !$isForeign
), ),
]; ];
} }
/**
* Years from marriage date or membership acquisition (whichever later).
* Partial year = full year.
*/
private static function calculateYears(?string $marriageDate, string $memberCreatedDate): int private static function calculateYears(?string $marriageDate, string $memberCreatedDate): int
{ {
if (!$marriageDate) { if (!$marriageDate) {
...@@ -178,12 +156,11 @@ final class SpouseFeeCalculator ...@@ -178,12 +156,11 @@ final class SpouseFeeCalculator
$marriageTs = strtotime($marriageDate); $marriageTs = strtotime($marriageDate);
$memberTs = strtotime($memberCreatedDate); $memberTs = strtotime($memberCreatedDate);
if (!$marriageTs || !$memberTs) { if (!$marriageTs || !$memberTs) {
return 1; return 1;
} }
// Use whichever is LATER
$startTs = max($marriageTs, $memberTs); $startTs = max($marriageTs, $memberTs);
$start = new \DateTime(date('Y-m-d', $startTs)); $start = new \DateTime(date('Y-m-d', $startTs));
$now = new \DateTime(); $now = new \DateTime();
...@@ -195,7 +172,6 @@ final class SpouseFeeCalculator ...@@ -195,7 +172,6 @@ final class SpouseFeeCalculator
$diff = $now->diff($start); $diff = $now->diff($start);
$years = $diff->y; $years = $diff->y;
// كسر السنة سنة كاملة
if ($diff->m > 0 || $diff->d > 0) { if ($diff->m > 0 || $diff->d > 0) {
$years++; $years++;
} }
...@@ -203,9 +179,6 @@ final class SpouseFeeCalculator ...@@ -203,9 +179,6 @@ final class SpouseFeeCalculator
return max(1, $years); return max(1, $years);
} }
/**
* Check if member got membership through transfer/separation/divorce/death/waiver.
*/
private static function isAcquiredMember(int $memberId): bool private static function isAcquiredMember(int $memberId): bool
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
...@@ -233,9 +206,6 @@ final class SpouseFeeCalculator ...@@ -233,9 +206,6 @@ final class SpouseFeeCalculator
return false; return false;
} }
/**
* Build detailed Arabic breakdown.
*/
private static function buildBreakdown( private static function buildBreakdown(
int $order, string $membershipValue, string $pct, string $pctFee, int $order, string $membershipValue, string $pct, string $pctFee,
string $annual, int $years, string $yearlyTotal, string $annual, int $years, string $yearlyTotal,
...@@ -251,12 +221,10 @@ final class SpouseFeeCalculator ...@@ -251,12 +221,10 @@ final class SpouseFeeCalculator
if (bccomp($pctFee, '0', 2) > 0) { if (bccomp($pctFee, '0', 2) > 0) {
$lines[] = "📊 نسبة {$pct}% × " . money($membershipValue) . ' = ' . money($pctFee); $lines[] = "📊 نسبة {$pct}% × " . money($membershipValue) . ' = ' . money($pctFee);
} }
if (bccomp($annual, '0', 2) > 0 && $years > 0) { if (bccomp($annual, '0', 2) > 0 && $years > 0) {
$lines[] = "📅 رسوم سنوية: {$annual} ج.م × {$years} سنة = " . money($yearlyTotal); $lines[] = "📅 رسوم سنوية: {$annual} ج.م × {$years} سنة = " . money($yearlyTotal);
$lines[] = ' (من تاريخ الزواج أو اكتساب العضوية — أيهما لاحق — كسر السنة سنة كاملة)'; $lines[] = ' (من تاريخ الزواج أو اكتساب العضوية — أيهما لاحق — كسر السنة سنة كاملة)';
} }
if (bccomp($formFee, '0', 2) > 0) { if (bccomp($formFee, '0', 2) > 0) {
$lines[] = '📝 رسوم استمارة إضافة: ' . money($formFee); $lines[] = '📝 رسوم استمارة إضافة: ' . money($formFee);
} }
...@@ -268,9 +236,6 @@ final class SpouseFeeCalculator ...@@ -268,9 +236,6 @@ final class SpouseFeeCalculator
return $lines; return $lines;
} }
/**
* Return error result.
*/
private static function error(string $message): array private static function error(string $message): array
{ {
return [ return [
...@@ -289,4 +254,4 @@ final class SpouseFeeCalculator ...@@ -289,4 +254,4 @@ final class SpouseFeeCalculator
'breakdown' => ['❌ ' . $message], 'breakdown' => ['❌ ' . $message],
]; ];
} }
} }
\ No newline at end of file
...@@ -5,6 +5,7 @@ namespace App\Modules\Temporary\Services; ...@@ -5,6 +5,7 @@ namespace App\Modules\Temporary\Services;
use App\Core\App; use App\Core\App;
use App\Modules\Rules\Services\RuleEngine; use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Members\Services\FormFeeService;
final class TemporaryFeeCalculator final class TemporaryFeeCalculator
{ {
...@@ -22,19 +23,20 @@ final class TemporaryFeeCalculator ...@@ -22,19 +23,20 @@ final class TemporaryFeeCalculator
} }
$hasChampionship = (bool) ($tempData['has_championship'] ?? false); $hasChampionship = (bool) ($tempData['has_championship'] ?? false);
$category = $tempData['category'] ?? '';
// Championship exemption
$exemptData = RuleEngine::get('TEMP_CHAMPIONSHIP_EXEMPT'); $exemptData = RuleEngine::get('TEMP_CHAMPIONSHIP_EXEMPT');
$isExempt = $hasChampionship && ($exemptData['exempt'] ?? true); $isExempt = $hasChampionship && ($exemptData['exempt'] ?? true);
if ($isExempt) { if ($isExempt) {
return [ return [
'fee' => '0.00', 'fee' => '0.00',
'percentage' => '0.00', 'percentage' => '0.00',
'rule_applied' => 'TEMP_CHAMPIONSHIP_EXEMPT', 'membership_value' => $membershipValue,
'exempt' => true, 'form_fee' => '0.00',
'error' => null, 'total_fee' => '0.00',
'rule_applied' => 'TEMP_CHAMPIONSHIP_EXEMPT',
'exempt' => true,
'error' => null,
]; ];
} }
...@@ -42,12 +44,7 @@ final class TemporaryFeeCalculator ...@@ -42,12 +44,7 @@ final class TemporaryFeeCalculator
$pct = $feeData['percentage'] ?? '10.00'; $pct = $feeData['percentage'] ?? '10.00';
$fee = bcmul($membershipValue, bcdiv($pct, '100', 4), 2); $fee = bcmul($membershipValue, bcdiv($pct, '100', 4), 2);
// Form fee if post-creation $formFee = FormFeeService::getFormFee($memberId, $member);
$formFee = '0.00';
if ($member['status'] !== 'potential') {
$formFeeData = RuleEngine::get('FORM_ADDITION_FEE');
$formFee = $formFeeData['amount'] ?? '570.00';
}
return [ return [
'membership_value' => $membershipValue, 'membership_value' => $membershipValue,
...@@ -66,14 +63,20 @@ final class TemporaryFeeCalculator ...@@ -66,14 +63,20 @@ final class TemporaryFeeCalculator
$errors = []; $errors = [];
$age = (int) ($tempData['age_years'] ?? 0); $age = (int) ($tempData['age_years'] ?? 0);
$maxAgeRules = [
'sister' => 'SISTER_MAX_AGE',
'stepchild' => 'STEPCHILD_MAX_AGE',
'orphan' => 'ORPHAN_MAX_AGE',
];
switch ($category) { switch ($category) {
case 'parent': case 'parent':
// No specific age limit for parents
break; break;
case 'special_needs': case 'special_needs':
if ($age < 21) { $minAge = (int) (RuleEngine::getValue('SPOUSE_WORKING_AGE_THRESHOLD', 'value') ?? 21);
$errors[] = 'أبناء ذوي الاحتياجات الخاصة يجب أن يكونوا فوق 21 سنة'; if ($age < $minAge) {
$errors[] = 'أبناء ذوي الاحتياجات الخاصة يجب أن يكونوا فوق ' . $minAge . ' سنة';
} }
if (empty($tempData['disability_documentation'])) { if (empty($tempData['disability_documentation'])) {
$errors[] = 'يجب تقديم وثائق الإعاقة'; $errors[] = 'يجب تقديم وثائق الإعاقة';
...@@ -87,8 +90,9 @@ final class TemporaryFeeCalculator ...@@ -87,8 +90,9 @@ final class TemporaryFeeCalculator
break; break;
case 'sister': case 'sister':
if ($age >= 25) { $maxAge = (int) (RuleEngine::getValue('SISTER_MAX_AGE', 'value') ?? 25);
$errors[] = 'شقيقة العضو يجب أن تكون أقل من 25 سنة'; if ($age >= $maxAge) {
$errors[] = 'شقيقة العضو يجب أن تكون أقل من ' . $maxAge . ' سنة';
} }
if (($tempData['gender'] ?? '') !== 'female') { if (($tempData['gender'] ?? '') !== 'female') {
$errors[] = 'هذه الفئة للإناث فقط'; $errors[] = 'هذه الفئة للإناث فقط';
...@@ -96,14 +100,16 @@ final class TemporaryFeeCalculator ...@@ -96,14 +100,16 @@ final class TemporaryFeeCalculator
break; break;
case 'stepchild': case 'stepchild':
if ($age >= 25) { $maxAge = (int) (RuleEngine::getValue('STEPCHILD_MAX_AGE', 'value') ?? 25);
$errors[] = 'أبناء الزوج/الزوجة يجب أن تكون أعمارهم أقل من 25 سنة'; if ($age >= $maxAge) {
$errors[] = 'أبناء الزوج/الزوجة يجب أن تكون أعمارهم أقل من ' . $maxAge . ' سنة';
} }
break; break;
case 'orphan': case 'orphan':
if ($age >= 25) { $maxAge = (int) (RuleEngine::getValue('ORPHAN_MAX_AGE', 'value') ?? 25);
$errors[] = 'الطفل اليتيم يجب أن يكون أقل من 25 سنة'; if ($age >= $maxAge) {
$errors[] = 'الطفل اليتيم يجب أن يكون أقل من ' . $maxAge . ' سنة';
} }
break; break;
...@@ -114,7 +120,6 @@ final class TemporaryFeeCalculator ...@@ -114,7 +120,6 @@ final class TemporaryFeeCalculator
break; break;
case 'nanny': case 'nanny':
// Nanny — companion, no specific age rules
break; break;
default: default:
...@@ -135,4 +140,5 @@ final class TemporaryFeeCalculator ...@@ -135,4 +140,5 @@ final class TemporaryFeeCalculator
$noIndependent = ['orphan', 'disabled_sibling', 'nanny']; $noIndependent = ['orphan', 'disabled_sibling', 'nanny'];
return !in_array($category, $noIndependent, true); return !in_array($category, $noIndependent, true);
} }
}
\ No newline at end of file }
<?php
declare(strict_types=1);
/**
* Phase 39: Add form-session rules + fix CHILD_INCLUDED_MAX_COUNT.
*
* Business requirement:
* - Initial membership form (505 EGP) includes 1 spouse + 2 children under 18 free.
* - After activation, any modification needs a 570 EGP "addition form".
* - Multiple additions within the SAME form session share one form fee.
* - Form validity days controls how long an addition form stays open.
*/
return function (\App\Core\Database $db) {
$now = date('Y-m-d H:i:s');
$today = date('Y-m-d');
// ── New rules ────────────────────────────────────────────────────
$newRules = [
[
'rule_code' => 'INITIAL_FREE_SPOUSES_COUNT',
'category' => 'spouse_fee',
'name_ar' => 'عدد الزوجات المشمولات مجاناً في الاستمارة الأولى',
'name_en' => 'Free Spouses on Initial Form',
'data_type' => 'integer',
'current_value_json' => '{"value":1}',
'parameters_json' => '{"value":"integer"}',
],
[
'rule_code' => 'INITIAL_FREE_CHILDREN_COUNT',
'category' => 'children_fee',
'name_ar' => 'عدد الأبناء المشمولين مجاناً في الاستمارة الأولى (تحت السن)',
'name_en' => 'Free Children on Initial Form',
'data_type' => 'integer',
'current_value_json' => '{"value":2}',
'parameters_json' => '{"value":"integer"}',
],
[
'rule_code' => 'ADDITION_FORM_VALIDITY_DAYS',
'category' => 'financial',
'name_ar' => 'مدة صلاحية استمارة الإضافة/التعديل (أيام)',
'name_en' => 'Addition Form Validity Days',
'data_type' => 'integer',
'current_value_json' => '{"value":30}',
'parameters_json' => '{"value":"integer"}',
],
];
foreach ($newRules as $rule) {
$exists = $db->selectOne(
"SELECT id FROM business_rules WHERE rule_code = ? AND branch_id IS NULL",
[$rule['rule_code']]
);
if (!$exists) {
$db->insert('business_rules', array_merge($rule, [
'branch_id' => null,
'effective_from' => $today,
'effective_to' => null,
'version' => 1,
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
]));
}
}
// ── Update CHILD_INCLUDED_MAX_COUNT from 3 → 2 ──────────────────
// The club rule is: first 2 children under 18 are free on the initial form.
// The existing seed had 3. We correct it here.
$childRule = $db->selectOne(
"SELECT id, current_value_json, version FROM business_rules WHERE rule_code = 'CHILD_INCLUDED_MAX_COUNT' AND branch_id IS NULL AND is_active = 1"
);
if ($childRule) {
$currentValue = json_decode($childRule['current_value_json'], true);
if (($currentValue['value'] ?? 3) === 3) {
$newVersion = (int) $childRule['version'] + 1;
$db->insert('rule_versions', [
'rule_id' => (int) $childRule['id'],
'version_number' => $newVersion,
'old_value_json' => $childRule['current_value_json'],
'new_value_json' => '{"value":2}',
'changed_by' => null,
'changed_at' => $now,
'change_reason' => 'تصحيح: القاعدة الصحيحة هي طفلين مشمولين مجاناً وليس 3',
]);
$db->update('business_rules', [
'current_value_json' => '{"value":2}',
'version' => $newVersion,
'updated_at' => $now,
], '`id` = ?', [(int) $childRule['id']]);
}
}
};
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