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,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