Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
Clubphp
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Clubphp
Commits
2b105af3
Commit
2b105af3
authored
May 10, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
koool
parent
1c230fd3
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
2050 additions
and
5 deletions
+2050
-5
AutoFreezeService.php
app/Modules/Members/Services/AutoFreezeService.php
+163
-0
MembershipRulesService.php
app/Modules/Members/Services/MembershipRulesService.php
+1191
-0
WaiverService.php
app/Modules/Members/Services/WaiverService.php
+165
-0
SubscriptionGenerator.php
app/Modules/Subscriptions/Services/SubscriptionGenerator.php
+18
-5
SeparationFeeCalculator.php
app/Modules/Transfers/Services/SeparationFeeCalculator.php
+25
-0
TransferEligibility.php
app/Modules/Transfers/Services/TransferEligibility.php
+180
-0
Phase_46_001_seed_subscription_rates_and_prices.php
...seeds/Phase_46_001_seed_subscription_rates_and_prices.php
+308
-0
No files found.
app/Modules/Members/Services/AutoFreezeService.php
0 → 100644
View file @
2b105af3
<?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
;
}
}
app/Modules/Members/Services/MembershipRulesService.php
0 → 100644
View file @
2b105af3
<?php
declare
(
strict_types
=
1
);
namespace
App\Modules\Members\Services
;
use
App\Core\App
;
use
App\Modules\Rules\Services\RuleEngine
;
final
class
MembershipRulesService
{
// ══════════════════════════════════════════════════════════════════════
// Member Type Classification (أنواع الأعضاء)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getMemberTypes
()
:
array
{
return
[
[
'code'
=>
'working'
,
'name_ar'
=>
'عضو عامل'
,
'name_en'
=>
'Working Member'
,
'min_age'
=>
21
,
'description'
=>
'العضو الأساسي الذي لا يقل سنه عن 21 عام'
,
],
[
'code'
=>
'dependent'
,
'name_ar'
=>
'عضو تابع'
,
'name_en'
=>
'Dependent Member'
,
'min_age'
=>
0
,
'description'
=>
'الأبناء والزوج/الزوجة أقل من 21 سنة'
,
],
[
'code'
=>
'temporary'
,
'name_ar'
=>
'عضو مؤقت'
,
'name_en'
=>
'Temporary Member'
,
'min_age'
=>
0
,
'description'
=>
'عضو مضاف بدون حق الفصل أو العضوية المستقلة'
,
],
[
'code'
=>
'honorary'
,
'name_ar'
=>
'عضو شرفي'
,
'name_en'
=>
'Honorary Member'
,
'min_age'
=>
0
,
'description'
=>
'عضوية لمدة سنة قابلة للتجديد بدون رسوم'
,
],
[
'code'
=>
'athletic'
,
'name_ar'
=>
'عضو رياضي'
,
'name_en'
=>
'Athletic Member'
,
'min_age'
=>
0
,
'description'
=>
'لاعب مسجل بأحد الألعاب لمدة 8 سنوات'
,
],
[
'code'
=>
'foreign'
,
'name_ar'
=>
'عضو أجنبي'
,
'name_en'
=>
'Foreign Member'
,
'min_age'
=>
21
,
'description'
=>
'غير حامل الجنسية المصرية — رسوم 10,000 دولار'
,
],
[
'code'
=>
'seasonal'
,
'name_ar'
=>
'عضو موسمي'
,
'name_en'
=>
'Seasonal Member'
,
'min_age'
=>
0
,
'description'
=>
'عضوية موسمية مؤقتة'
,
],
];
}
public
static
function
classifyMemberType
(
string
$relationship
,
int
$age
)
:
string
{
if
(
$relationship
===
'self'
)
{
return
$age
>=
21
?
'working'
:
'dependent'
;
}
if
(
in_array
(
$relationship
,
[
'spouse'
,
'husband'
,
'wife'
]))
{
return
$age
>=
21
?
'working'
:
'dependent'
;
}
if
(
in_array
(
$relationship
,
[
'child'
,
'son'
,
'daughter'
]))
{
return
'dependent'
;
}
return
'temporary'
;
}
public
static
function
validateMinAge
(
string
$memberType
,
int
$age
)
:
array
{
if
(
$memberType
===
'working'
&&
$age
<
21
)
{
return
[
'valid'
=>
false
,
'reason'
=>
'العضو العامل الأساسي لا يقل سنه عن 21 عام'
];
}
return
[
'valid'
=>
true
];
}
// ══════════════════════════════════════════════════════════════════════
// Form Fees (رسوم الاستمارات)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getNewMembershipFormFee
()
:
array
{
$override
=
RuleEngine
::
get
(
'membership.new_form_fee'
);
$formAmount
=
(
$override
[
'form_amount'
]
??
'500'
);
$martyrsStamp
=
(
$override
[
'martyrs_stamp'
]
??
'5'
);
$total
=
bcadd
(
$formAmount
,
$martyrsStamp
,
2
);
return
[
'form_amount'
=>
$formAmount
,
'martyrs_stamp'
=>
$martyrsStamp
,
'total'
=>
$total
,
'description'
=>
'استمارة العضوية الجديدة 500 جنيه + 5 طابع شهداء'
,
];
}
public
static
function
getAdditionFormFee
()
:
array
{
$override
=
RuleEngine
::
get
(
'membership.addition_form_fee'
);
$formFee
=
(
$override
[
'amount'
]
??
'570'
);
$annualSub
=
self
::
getAnnualRates
(
self
::
currentFinancialYear
())[
'member'
]
??
'492'
;
$devFee
=
self
::
getAnnualRates
(
self
::
currentFinancialYear
())[
'dev'
]
??
'35'
;
$totalAnnual
=
bcadd
(
$annualSub
,
$devFee
,
2
);
$total
=
bcadd
(
$formFee
,
$totalAnnual
,
2
);
return
[
'form_fee'
=>
$formFee
,
'annual_subscription'
=>
$totalAnnual
,
'total'
=>
$total
,
'description'
=>
'أي إضافة بعد إنشاء العضوية: 570 جنيه + الاشتراك السنوي'
,
];
}
public
static
function
getIncludedInMembership
()
:
array
{
return
[
'spouse_count'
=>
1
,
'children_under_18'
=>
3
,
'description'
=>
'تشمل العضوية: زوج + زوجة + 3 أبناء تحت 18 عام'
,
];
}
// ══════════════════════════════════════════════════════════════════════
// Membership Pricing
// ══════════════════════════════════════════════════════════════════════
public
static
function
getMembershipPrice
(
string
$qualificationLevel
,
?
string
$asOfDate
=
null
)
:
string
{
$asOfDate
=
$asOfDate
??
date
(
'Y-m-d'
);
$cutoff
=
'2024-07-01'
;
$override
=
RuleEngine
::
get
(
'membership.price.'
.
$qualificationLevel
,
null
,
$asOfDate
);
if
(
$override
!==
null
)
{
return
(
string
)
(
$override
[
'amount'
]
??
$override
[
'value'
]
??
'0'
);
}
if
(
$asOfDate
<
$cutoff
)
{
return
match
(
$qualificationLevel
)
{
'high'
,
'عالى'
=>
'114000'
,
'medium'
,
'متوسط'
=>
'171000'
,
'none'
,
'بدون'
=>
'228000'
,
default
=>
'0'
,
};
}
return
match
(
$qualificationLevel
)
{
'high'
,
'عالى'
=>
'150000'
,
'medium'
,
'متوسط'
=>
'225000'
,
'none'
,
'بدون'
=>
'300000'
,
default
=>
'0'
,
};
}
// ══════════════════════════════════════════════════════════════════════
// Children Fees (أبناء العضو العامل)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getChildAdditionFee
(
int
$childAge
,
string
$membershipValue
)
:
array
{
$override
=
RuleEngine
::
get
(
'membership.child_fee.'
.
$childAge
);
if
(
$override
!==
null
&&
isset
(
$override
[
'percentage'
]))
{
$pct
=
(
string
)
$override
[
'percentage'
];
return
[
'percentage'
=>
$pct
,
'amount'
=>
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
),
'type'
=>
$override
[
'type'
]
??
'regular'
,
];
}
if
(
$childAge
<
18
)
{
// 4th child under 18
$pct
=
'5'
;
return
[
'percentage'
=>
$pct
,
'amount'
=>
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
),
'type'
=>
'regular'
,
];
}
if
(
$childAge
===
18
)
{
$pct
=
'10'
;
}
elseif
(
$childAge
===
19
)
{
$pct
=
'15'
;
}
elseif
(
$childAge
===
20
)
{
$pct
=
'20'
;
}
else
{
// 21+: temporary until 25
$pct
=
'15'
;
return
[
'percentage'
=>
$pct
,
'amount'
=>
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
),
'type'
=>
'temporary'
,
];
}
return
[
'percentage'
=>
$pct
,
'amount'
=>
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
),
'type'
=>
'regular'
,
];
}
// ══════════════════════════════════════════════════════════════════════
// Acquired Member Children (أبناء العضو مكتسب العضوية بعد الانفصال)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getAcquiredMemberChildFee
(
int
$childAge
,
string
$membershipValue
)
:
array
{
$override
=
RuleEngine
::
get
(
'membership.acquired_child_fee.'
.
$childAge
);
if
(
$override
!==
null
&&
isset
(
$override
[
'percentage'
]))
{
$pct
=
(
string
)
$override
[
'percentage'
];
return
[
'percentage'
=>
$pct
,
'amount'
=>
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
),
];
}
if
(
$childAge
<
12
)
{
$pct
=
'15'
;
}
elseif
(
$childAge
<
16
)
{
$pct
=
'20'
;
}
elseif
(
$childAge
<
18
)
{
$pct
=
'25'
;
}
else
{
$pct
=
'30'
;
}
return
[
'percentage'
=>
$pct
,
'amount'
=>
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
),
];
}
// ══════════════════════════════════════════════════════════════════════
// Spouse Fees (زوجات العضو العامل)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getSpouseAdditionFee
(
int
$spouseOrder
,
string
$membershipValue
,
?
string
$marriageOrMembershipDate
=
null
,
bool
$isForeign
=
false
)
:
array
{
$override
=
RuleEngine
::
get
(
'membership.spouse_fee.'
.
$spouseOrder
);
if
(
$override
!==
null
&&
isset
(
$override
[
'percentage'
]))
{
return
$override
;
}
if
(
$spouseOrder
===
1
&&
!
$isForeign
)
{
return
[
'percentage'
=>
'0'
,
'amount'
=>
'0.00'
,
'annual_fee'
=>
'0.00'
,
'years'
=>
0
,
'total'
=>
'0.00'
,
];
}
if
(
$isForeign
&&
$spouseOrder
===
1
)
{
$pct
=
'15'
;
$amount
=
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
);
return
[
'percentage'
=>
$pct
,
'amount'
=>
$amount
,
'annual_fee'
=>
'0.00'
,
'years'
=>
0
,
'total'
=>
$amount
,
];
}
$config
=
match
(
$spouseOrder
)
{
2
=>
[
'percentage'
=>
'10'
,
'annual_fee'
=>
'150'
],
3
=>
[
'percentage'
=>
'20'
,
'annual_fee'
=>
'200'
],
default
=>
[
'percentage'
=>
'30'
,
'annual_fee'
=>
'300'
],
};
$baseAmount
=
bcdiv
(
bcmul
(
$membershipValue
,
$config
[
'percentage'
],
4
),
'100'
,
2
);
$years
=
0
;
$annualTotal
=
'0.00'
;
if
(
$marriageOrMembershipDate
!==
null
)
{
$start
=
new
\DateTime
(
$marriageOrMembershipDate
);
$now
=
new
\DateTime
();
$diff
=
$start
->
diff
(
$now
);
$years
=
$diff
->
y
;
if
(
$diff
->
m
>
0
||
$diff
->
d
>
0
)
{
$years
++
;
}
$annualTotal
=
bcmul
((
string
)
$years
,
$config
[
'annual_fee'
],
2
);
}
$total
=
bcadd
(
$baseAmount
,
$annualTotal
,
2
);
return
[
'percentage'
=>
$config
[
'percentage'
],
'amount'
=>
$baseAmount
,
'annual_fee'
=>
$config
[
'annual_fee'
],
'years'
=>
$years
,
'total'
=>
$total
,
];
}
// ══════════════════════════════════════════════════════════════════════
// Acquired Member Spouse Fee
// ══════════════════════════════════════════════════════════════════════
public
static
function
getAcquiredMemberSpouseFee
(
int
$spouseOrder
,
string
$membershipValue
)
:
array
{
$override
=
RuleEngine
::
get
(
'membership.acquired_spouse_fee.'
.
$spouseOrder
);
if
(
$override
!==
null
&&
isset
(
$override
[
'percentage'
]))
{
$pct
=
(
string
)
$override
[
'percentage'
];
return
[
'percentage'
=>
$pct
,
'amount'
=>
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
),
];
}
$pct
=
match
(
$spouseOrder
)
{
1
=>
'50'
,
default
=>
'75'
,
};
return
[
'percentage'
=>
$pct
,
'amount'
=>
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
),
];
}
// ══════════════════════════════════════════════════════════════════════
// Separation/Transfer Fees (رسوم الفصل)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getSeparationFee
(
int
$yearsSinceAddition
,
string
$currentMembershipValue
)
:
array
{
$override
=
RuleEngine
::
get
(
'membership.separation_fee'
);
$formFee
=
'570'
;
if
(
$override
!==
null
&&
isset
(
$override
[
'tiers'
]))
{
$tiers
=
$override
[
'tiers'
];
}
else
{
$tiers
=
[
1
=>
'30'
,
2
=>
'20'
,
3
=>
'15'
,
4
=>
'10'
,
5
=>
'5'
,
];
}
if
(
isset
(
$tiers
[
$yearsSinceAddition
]))
{
$pct
=
(
string
)
$tiers
[
$yearsSinceAddition
];
}
elseif
(
$yearsSinceAddition
>=
6
)
{
$pct
=
'2.5'
;
}
else
{
$pct
=
'30'
;
}
$separationAmount
=
bcdiv
(
bcmul
(
$currentMembershipValue
,
$pct
,
4
),
'100'
,
2
);
$annualSubscription
=
self
::
getAnnualRates
(
self
::
currentFinancialYear
())[
'member'
]
??
'410'
;
return
[
'percentage'
=>
$pct
,
'separation_amount'
=>
$separationAmount
,
'form_fee'
=>
$formFee
,
'annual_subscription'
=>
$annualSubscription
,
'total'
=>
bcadd
(
bcadd
(
$separationAmount
,
$formFee
,
2
),
$annualSubscription
,
2
),
];
}
// ══════════════════════════════════════════════════════════════════════
// Transfer Eligibility
// ══════════════════════════════════════════════════════════════════════
public
static
function
canChildSeparate
(
string
$gender
,
int
$age
,
?
bool
$isEmployed
=
null
,
?
bool
$isMarried
=
null
)
:
array
{
if
(
$gender
===
'female'
)
{
if
(
$isMarried
===
true
)
{
return
[
'eligible'
=>
true
,
'reason'
=>
'ابنة متزوجة — يحق لها الفصل'
];
}
return
[
'eligible'
=>
false
,
'reason'
=>
'الابنة غير متزوجة — لا يحق لها الفصل'
];
}
// Male
if
(
$age
>=
25
)
{
return
[
'eligible'
=>
true
,
'reason'
=>
'ابن بلغ 25 سنة — يحق له الفصل'
];
}
if
(
$isEmployed
===
true
)
{
return
[
'eligible'
=>
true
,
'reason'
=>
'ابن متخرج وموظف — يحق له الفصل'
];
}
return
[
'eligible'
=>
false
,
'reason'
=>
'ابن لم يبلغ 25 سنة وغير موظف — لا يحق له الفصل'
];
}
// ══════════════════════════════════════════════════════════════════════
// Divorce Transfer Rules
// ══════════════════════════════════════════════════════════════════════
public
static
function
getDivorceTransferFee
(
string
$divorceScenario
,
string
$membershipValue
,
?
int
$yearsSinceMembership
=
null
)
:
array
{
$annualSubscription
=
self
::
getAnnualRates
(
self
::
currentFinancialYear
())[
'member'
]
??
'410'
;
switch
(
$divorceScenario
)
{
case
'both_working_before_marriage'
:
return
[
'type'
=>
'annual_subscription_only'
,
'amount'
=>
$annualSubscription
,
'membership_basis'
=>
'working_member'
,
'conditions'
=>
self
::
getDivorceConditions
(
$yearsSinceMembership
),
];
case
'same_form'
:
$pct
=
'10'
;
$amount
=
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
);
return
[
'type'
=>
'percentage'
,
'percentage'
=>
$pct
,
'amount'
=>
$amount
,
'membership_basis'
=>
'working_member'
,
'conditions'
=>
self
::
getDivorceConditions
(
$yearsSinceMembership
),
];
case
'joined_after'
:
$pct
=
'50'
;
$amount
=
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
);
return
[
'type'
=>
'percentage'
,
'percentage'
=>
$pct
,
'amount'
=>
$amount
,
'membership_basis'
=>
'acquired_member'
,
'conditions'
=>
self
::
getDivorceConditions
(
$yearsSinceMembership
),
];
default
:
return
[
'type'
=>
'unknown'
,
'amount'
=>
'0.00'
,
'conditions'
=>
[],
];
}
}
private
static
function
getDivorceConditions
(
?
int
$yearsSinceMembership
)
:
array
{
$conditions
=
[];
$conditions
[]
=
[
'rule'
=>
'divorce_within_one_year'
,
'description'
=>
'يجب أن يكون الطلاق قد تم خلال عام واحد'
,
];
$conditions
[]
=
[
'rule'
=>
'membership_min_5_years'
,
'description'
=>
'مضى على العضوية 5 سنوات على الأقل (يُعفى إذا وُجد أبناء)'
,
'met'
=>
$yearsSinceMembership
!==
null
?
$yearsSinceMembership
>=
5
:
null
,
'waivable'
=>
true
,
'waiver_reason'
=>
'وجود أبناء'
,
];
return
$conditions
;
}
// ══════════════════════════════════════════════════════════════════════
// Freeze Rules
// ══════════════════════════════════════════════════════════════════════
public
static
function
shouldFreezeMaleChild
(
int
$age
)
:
bool
{
return
$age
>=
25
;
}
public
static
function
isFreezeActive
(
array
$child
)
:
bool
{
if
((
$child
[
'gender'
]
??
''
)
!==
'male'
)
{
return
false
;
}
$age
=
(
int
)
(
$child
[
'age_years'
]
??
0
);
if
(
$age
<
25
)
{
return
false
;
}
$status
=
$child
[
'status'
]
??
$child
[
'membership_status'
]
??
''
;
return
in_array
(
$status
,
[
'frozen'
,
'مجمد'
],
true
);
}
// ══════════════════════════════════════════════════════════════════════
// Death Transfer
// ══════════════════════════════════════════════════════════════════════
public
static
function
getDeathTransferRules
()
:
array
{
$annualSubscription
=
self
::
getAnnualRates
(
self
::
currentFinancialYear
())[
'member'
]
??
'410'
;
return
[
'beneficiary'
=>
'spouse'
,
'retains_number'
=>
true
,
'form_fee'
=>
'570'
,
'annual_subscription'
=>
$annualSubscription
,
'membership_fee'
=>
'0.00'
,
'description'
=>
'تنتقل العضوية للزوج/الزوجة بنفس الرقم مع سداد رسوم الاستمارة والاشتراك السنوي فقط'
,
];
}
// ══════════════════════════════════════════════════════════════════════
// Waiver (التنازل)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getWaiverFee
(
string
$membershipValue
)
:
array
{
$override
=
RuleEngine
::
get
(
'membership.waiver_fee'
);
$pct
=
(
$override
!==
null
&&
isset
(
$override
[
'percentage'
]))
?
(
string
)
$override
[
'percentage'
]
:
'30'
;
$amount
=
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
);
return
[
'percentage'
=>
$pct
,
'amount'
=>
$amount
,
];
}
public
static
function
getWaiverConditions
()
:
array
{
return
[
[
'code'
=>
'board_approval'
,
'description'
=>
'موافقة مجلس الأمناء'
,
'required'
=>
true
,
],
[
'code'
=>
'dependants_limit'
,
'description'
=>
'لا يزيد عدد الأعضاء للعضو المتنازل له عن عدد الأعضاء التابعين للعضو المتنازل'
,
'required'
=>
true
,
],
[
'code'
=>
'extra_dependants_fee'
,
'description'
=>
'في حالة رغبة المتنازل له في إضافة أعضاء بالزيادة يلتزم بسداد الرسوم المحددة من مجلس الأمناء'
,
'required'
=>
false
,
],
[
'code'
=>
'annual_subscription_paid'
,
'description'
=>
'التجديد السنوي قبل إتمام التنازل'
,
'required'
=>
true
,
],
[
'code'
=>
'new_form_required'
,
'description'
=>
'استمارة عضوية جديدة للعضو المتنازل له'
,
'required'
=>
true
,
],
[
'code'
=>
'same_membership_number'
,
'description'
=>
'يصبح العضو الأساسي بنفس رقم العضوية المتنازل'
,
'required'
=>
true
,
],
[
'code'
=>
'archive_old_data'
,
'description'
=>
'يتم نقل جميع البيانات القديمة إلى الأرشيف للحفظ'
,
'required'
=>
true
,
],
];
}
public
static
function
getWaiverProcess
()
:
array
{
return
[
'fee_percentage'
=>
'30'
,
'retains_number'
=>
true
,
'requires_new_form'
=>
true
,
'archive_source_data'
=>
true
,
'steps'
=>
[
'طلب تنازل مع موافقة مجلس الأمناء'
,
'سداد 30% من قيمة العضوية وقت التنازل'
,
'استمارة عضوية جديدة للمتنازل له'
,
'سداد التجديد السنوي'
,
'أرشفة بيانات العضو المتنازل'
,
'تسجيل المتنازل له بنفس رقم العضوية'
,
],
];
}
// ══════════════════════════════════════════════════════════════════════
// Temporary Members (العضو المؤقت)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getTemporaryMemberFee
(
string
$membershipValue
)
:
string
{
$override
=
RuleEngine
::
get
(
'membership.temporary_fee'
);
$pct
=
(
$override
!==
null
&&
isset
(
$override
[
'percentage'
]))
?
(
string
)
$override
[
'percentage'
]
:
'10'
;
return
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
);
}
public
static
function
getTemporaryMemberCategories
()
:
array
{
return
[
[
'code'
=>
'special_needs_over_21'
,
'name_ar'
=>
'أبناء ذوي الاحتياجات الخاصة فوق 21 سنة'
,
'fee_applies'
=>
true
,
'can_separate'
=>
false
,
],
[
'code'
=>
'parents'
,
'name_ar'
=>
'والدا العضو العامل الأصلي'
,
'fee_applies'
=>
true
,
'can_separate'
=>
false
,
],
[
'code'
=>
'unmarried_unemployed_daughters'
,
'name_ar'
=>
'بنات العضو الغير متزوجات واللاتي لا يعملن بأجر'
,
'fee_applies'
=>
true
,
'can_separate'
=>
false
,
],
[
'code'
=>
'sisters_under_25'
,
'name_ar'
=>
'شقيقات العضو أقل من 25 سنة (غير متزوجات/مطلقات/أرامل، غير عاملات، مقيمات معه، وهو عائلهن الوحيد)'
,
'fee_applies'
=>
true
,
'can_separate'
=>
false
,
'max_age'
=>
25
,
],
[
'code'
=>
'stepchildren_under_25'
,
'name_ar'
=>
'أبناء الزوج/الزوجة من غير أبناء العضو العامل (أقل من 25 سنة)'
,
'fee_applies'
=>
true
,
'can_separate'
=>
false
,
'max_age'
=>
25
,
],
[
'code'
=>
'orphan_sponsored'
,
'name_ar'
=>
'الطفل اليتيم الذي تكفله أسرة العضو (قانون 12/1996) حتى 25 عام'
,
'fee_applies'
=>
true
,
'fee_amount'
=>
'غير محدد حالياً'
,
'can_separate'
=>
false
,
'max_age'
=>
25
,
],
[
'code'
=>
'disabled_sibling'
,
'name_ar'
=>
'شقيق العضو المعاق (بشرط أن يكون العضو عائله الوحيد)'
,
'fee_applies'
=>
true
,
'fee_amount'
=>
'غير محدد حالياً'
,
'can_separate'
=>
false
,
],
[
'code'
=>
'nanny'
,
'name_ar'
=>
'المربية'
,
'fee_percentage'
=>
'5'
,
'fee_applies'
=>
true
,
'can_separate'
=>
false
,
],
[
'code'
=>
'championship_exempt'
,
'name_ar'
=>
'حاصل على بطولات جمهورية (بدون رسوم إضافة)'
,
'fee_applies'
=>
false
,
'can_separate'
=>
false
,
],
];
}
public
static
function
getCompanionFee
(
string
$categoryCode
,
string
$membershipValue
)
:
array
{
$categories
=
self
::
getTemporaryMemberCategories
();
$category
=
null
;
foreach
(
$categories
as
$cat
)
{
if
(
$cat
[
'code'
]
===
$categoryCode
)
{
$category
=
$cat
;
break
;
}
}
if
(
!
$category
)
{
return
[
'error'
=>
'فئة غير معروفة'
,
'fee'
=>
'0.00'
];
}
if
(
!
$category
[
'fee_applies'
])
{
return
[
'fee'
=>
'0.00'
,
'category'
=>
$category
[
'name_ar'
],
'exempt'
=>
true
];
}
if
(
isset
(
$category
[
'fee_percentage'
]))
{
$pct
=
$category
[
'fee_percentage'
];
$fee
=
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
);
return
[
'fee'
=>
$fee
,
'percentage'
=>
$pct
,
'category'
=>
$category
[
'name_ar'
]];
}
// Default: 10% for temporary members
$pct
=
'10'
;
$fee
=
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
);
return
[
'fee'
=>
$fee
,
'percentage'
=>
$pct
,
'category'
=>
$category
[
'name_ar'
]];
}
// ══════════════════════════════════════════════════════════════════════
// Honorary Member (العضو الشرفي)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getHonoraryMemberRules
()
:
array
{
return
[
'duration_years'
=>
1
,
'renewable'
=>
true
,
'fees'
=>
'0.00'
,
'annual_subscription'
=>
'0.00'
,
'requires_board_approval'
=>
true
,
'conditions'
=>
[
'قرار من مجلس الأمناء'
,
'نظير خدمات جليلة للدولة أو المنشأة الرياضية'
,
],
'description'
=>
'عضوية لمدة سنة واحدة قابلة للتجديد بدون رسوم أو اشتراكات'
,
];
}
// ══════════════════════════════════════════════════════════════════════
// Athletic Member (العضو الرياضي)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getAthleticMemberConversionRules
()
:
array
{
$override
=
RuleEngine
::
get
(
'membership.athletic_conversion'
);
return
[
'conversion_percentage'
=>
(
$override
[
'percentage'
]
??
'50'
),
'min_playing_years'
=>
(
$override
[
'min_years'
]
??
8
),
'conditions'
=>
[
'مرور 8 سنوات على الأقل لاعباً بأحد الألعاب بالمدينة الرياضية'
,
'مسجل باتحاد اللعبة باسم المدينة الرياضية'
,
'تمثيل المدينة الرياضية في أعلى مستوى تنافسي طوال المدة'
,
],
'description'
=>
'رسوم تحويل 50% من قيمة العضوية الجديدة وقت تقديم الطلب'
,
];
}
public
static
function
getAthleticConversionFee
(
string
$membershipValue
)
:
array
{
$rules
=
self
::
getAthleticMemberConversionRules
();
$pct
=
$rules
[
'conversion_percentage'
];
$fee
=
bcdiv
(
bcmul
(
$membershipValue
,
$pct
,
4
),
'100'
,
2
);
return
[
'percentage'
=>
$pct
,
'fee'
=>
$fee
,
'conditions'
=>
$rules
[
'conditions'
],
];
}
// ══════════════════════════════════════════════════════════════════════
// Foreign Member (العضو الأجنبي)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getForeignMemberRules
()
:
array
{
$override
=
RuleEngine
::
get
(
'membership.foreign_member'
);
return
[
'fee_usd'
=>
(
$override
[
'fee_usd'
]
??
'10000'
),
'currency'
=>
'USD'
,
'includes'
=>
'زوج + زوجة + 3 أبناء'
,
'has_full_rights'
=>
true
,
'can_separate'
=>
true
,
'can_transfer'
=>
true
,
'description'
=>
'الذي لا يحمل الجنسية المصرية — رسوم 10,000 دولار شاملة العائلة'
,
];
}
// ══════════════════════════════════════════════════════════════════════
// Annual Subscription Rates
// ══════════════════════════════════════════════════════════════════════
public
static
function
getAnnualRates
(
string
$financialYear
)
:
array
{
$override
=
RuleEngine
::
get
(
'membership.annual_rates.'
.
$financialYear
);
if
(
$override
!==
null
)
{
return
$override
;
}
return
match
(
$financialYear
)
{
'2023/2024'
=>
[
'member'
=>
'410'
,
'spouse'
=>
'410'
,
'child'
=>
'185'
,
'temp'
=>
'185'
,
'dev'
=>
'35'
,
'note'
=>
'خصم 50% مطبق'
,
],
'2024/2025'
=>
[
'member'
=>
'410'
,
'spouse'
=>
'410'
,
'child'
=>
'185'
,
'temp'
=>
'185'
,
'dev'
=>
'35'
,
],
'2025/2026'
=>
[
'member'
=>
'492'
,
'spouse'
=>
'492'
,
'child'
=>
'222'
,
'temp'
=>
'222'
,
'dev'
=>
'35'
,
'note'
=>
'زيادة 20%'
,
],
default
=>
[
'member'
=>
'492'
,
'spouse'
=>
'492'
,
'child'
=>
'222'
,
'temp'
=>
'222'
,
'dev'
=>
'35'
,
],
};
}
// ══════════════════════════════════════════════════════════════════════
// Installment Rules
// ══════════════════════════════════════════════════════════════════════
public
static
function
getInstallmentTerms
(
string
$membershipValue
,
string
$downPayment
)
:
array
{
$override
=
RuleEngine
::
get
(
'membership.installment_terms'
)
??
[];
$minDownPct
=
!
empty
(
$override
[
'min_down_percentage'
])
?
(
string
)
$override
[
'min_down_percentage'
]
:
'25'
;
$maxMonths
=
!
empty
(
$override
[
'max_months'
])
?
(
int
)
$override
[
'max_months'
]
:
30
;
$annualInterest
=
!
empty
(
$override
[
'annual_interest'
])
?
(
string
)
$override
[
'annual_interest'
]
:
'22'
;
$cashGraceDays
=
!
empty
(
$override
[
'cash_grace_days'
])
?
(
int
)
$override
[
'cash_grace_days'
]
:
30
;
$minDownAmount
=
bcdiv
(
bcmul
(
$membershipValue
,
$minDownPct
,
4
),
'100'
,
2
);
$downPaymentValid
=
bccomp
(
$downPayment
,
$minDownAmount
,
2
)
>=
0
;
$remaining
=
bcsub
(
$membershipValue
,
$downPayment
,
2
);
if
(
bccomp
(
$remaining
,
'0'
,
2
)
<
0
)
{
$remaining
=
'0.00'
;
}
$monthlyInterestRate
=
bcdiv
(
$annualInterest
,
'1200'
,
8
);
$totalInterest
=
bcdiv
(
bcmul
(
bcmul
(
$remaining
,
$annualInterest
,
4
),
(
string
)
$maxMonths
,
4
),
'1200'
,
2
);
$totalWithInterest
=
bcadd
(
$remaining
,
$totalInterest
,
2
);
$monthlyInstallment
=
$maxMonths
>
0
?
bcdiv
(
$totalWithInterest
,
(
string
)
$maxMonths
,
2
)
:
'0.00'
;
return
[
'membership_value'
=>
$membershipValue
,
'min_down_percentage'
=>
$minDownPct
,
'min_down_amount'
=>
$minDownAmount
,
'down_payment'
=>
$downPayment
,
'down_payment_valid'
=>
$downPaymentValid
,
'remaining'
=>
$remaining
,
'max_months'
=>
$maxMonths
,
'annual_interest'
=>
$annualInterest
,
'monthly_interest_rate'
=>
$monthlyInterestRate
,
'total_interest'
=>
$totalInterest
,
'total_with_interest'
=>
$totalWithInterest
,
'monthly_installment'
=>
$monthlyInstallment
,
'cash_grace_days'
=>
$cashGraceDays
,
'cash_note'
=>
'السداد الكامل خلال '
.
$cashGraceDays
.
' يوم بدون فوائد'
,
];
}
// ══════════════════════════════════════════════════════════════════════
// Penalties & Violations (مخالفات الأعضاء ومساءلتهم)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getViolationPenalties
()
:
array
{
return
[
[
'code'
=>
'attention'
,
'name_ar'
=>
'لفت نظر'
,
'severity'
=>
1
,
'financial'
=>
false
],
[
'code'
=>
'warning'
,
'name_ar'
=>
'إنذار'
,
'severity'
=>
2
,
'financial'
=>
false
],
[
'code'
=>
'fine'
,
'name_ar'
=>
'غرامة'
,
'severity'
=>
3
,
'financial'
=>
true
,
'min'
=>
'1000'
,
'max'
=>
'10000'
],
[
'code'
=>
'suspension'
,
'name_ar'
=>
'إيقاف عن مزاولة النشاط'
,
'severity'
=>
4
,
'financial'
=>
false
,
'max_duration_months'
=>
6
],
[
'code'
=>
'ban'
,
'name_ar'
=>
'حرمان من دخول النادي'
,
'severity'
=>
5
,
'financial'
=>
false
,
'max_duration_months'
=>
6
],
[
'code'
=>
'expulsion'
,
'name_ar'
=>
'فصل العضو'
,
'severity'
=>
6
,
'financial'
=>
false
,
'no_refund'
=>
true
],
];
}
public
static
function
validateFineAmount
(
string
$amount
)
:
array
{
$min
=
'1000'
;
$max
=
'10000'
;
$override
=
RuleEngine
::
get
(
'membership.fine_limits'
);
if
(
$override
)
{
$min
=
(
string
)
(
$override
[
'min'
]
??
'1000'
);
$max
=
(
string
)
(
$override
[
'max'
]
??
'10000'
);
}
if
(
bccomp
(
$amount
,
$min
,
2
)
<
0
)
{
return
[
'valid'
=>
false
,
'reason'
=>
'الغرامة لا تقل عن '
.
$min
.
' جنيه'
];
}
if
(
bccomp
(
$amount
,
$max
,
2
)
>
0
)
{
return
[
'valid'
=>
false
,
'reason'
=>
'الغرامة لا تزيد عن '
.
$max
.
' جنيه'
];
}
return
[
'valid'
=>
true
];
}
public
static
function
canAppeal
(
string
$penaltyDate
)
:
bool
{
$deadline
=
(
new
\DateTime
(
$penaltyDate
))
->
modify
(
'+15 days'
);
return
new
\DateTime
()
<=
$deadline
;
}
public
static
function
getAppealDeadline
(
string
$penaltyDate
)
:
string
{
return
(
new
\DateTime
(
$penaltyDate
))
->
modify
(
'+15 days'
)
->
format
(
'Y-m-d'
);
}
public
static
function
canRequestReview
(
string
$expulsionDate
)
:
bool
{
$eligibleDate
=
(
new
\DateTime
(
$expulsionDate
))
->
modify
(
'+6 months'
);
return
new
\DateTime
()
>=
$eligibleDate
;
}
public
static
function
getReviewEligibleDate
(
string
$expulsionDate
)
:
string
{
return
(
new
\DateTime
(
$expulsionDate
))
->
modify
(
'+6 months'
)
->
format
(
'Y-m-d'
);
}
// ══════════════════════════════════════════════════════════════════════
// Fines & Penalties Accumulation (الغرامات)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getFineAccumulationRules
()
:
array
{
return
[
'max_accumulation_years'
=>
5
,
'drop_after_max'
=>
true
,
'description'
=>
'تطبق غرامات وتستمر في التراكم لمدة 5 سنوات كحد أقصى — بعدها إسقاط العضوية'
,
];
}
// ══════════════════════════════════════════════════════════════════════
// Membership Termination & Drop (انتهاء وإسقاط العضوية)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getMembershipEndReasons
()
:
array
{
return
[
[
'code'
=>
'death'
,
'name_ar'
=>
'الوفاة'
,
'type'
=>
'termination'
],
[
'code'
=>
'waiver'
,
'name_ar'
=>
'التنازل عن العضوية'
,
'type'
=>
'termination'
],
[
'code'
=>
'board_decision'
,
'name_ar'
=>
'إنهاء العضوية بقرار مجلس أمناء'
,
'type'
=>
'termination'
],
[
'code'
=>
'condition_lost'
,
'name_ar'
=>
'فقد شرط من شروط العضوية'
,
'type'
=>
'drop'
],
[
'code'
=>
'expulsion'
,
'name_ar'
=>
'الفصل من العضوية طبقاً للائحة'
,
'type'
=>
'drop'
],
[
'code'
=>
'installment_default'
,
'name_ar'
=>
'عدم الالتزام بسداد الأقساط المستحقة'
,
'type'
=>
'drop'
],
[
'code'
=>
'unpaid_5_years'
,
'name_ar'
=>
'تأخر عن سداد الاشتراك السنوي 5 سنوات متتالية'
,
'type'
=>
'drop'
],
];
}
public
static
function
shouldDropMembership
(
int
$unpaidYears
,
bool
$hasUnpaidInstallments
=
false
)
:
bool
{
if
(
$hasUnpaidInstallments
)
{
return
true
;
}
return
$unpaidYears
>=
5
;
}
public
static
function
canReinstate
(
string
$dropDate
)
:
array
{
$deadline
=
(
new
\DateTime
(
$dropDate
))
->
modify
(
'+1 year'
);
$eligible
=
new
\DateTime
()
<=
$deadline
;
return
[
'eligible'
=>
$eligible
,
'deadline'
=>
$deadline
->
format
(
'Y-m-d'
),
'requires'
=>
[
'سداد جميع المبالغ المتأخرة'
,
'ما يحدده مجلس الأمناء من غرامات'
,
],
'board_approval'
=>
true
,
'description'
=>
$eligible
?
'يجوز إعادة العضوية خلال سنة من تاريخ الإسقاط بعد السداد وموافقة المجلس'
:
'انتهت مهلة السنة — لا يمكن إعادة العضوية'
,
];
}
public
static
function
getBlockNonPayingMemberRules
()
:
array
{
return
[
'block_entry'
=>
true
,
'block_carnet'
=>
true
,
'block_subscription'
=>
true
,
'description'
=>
'منع دخول الأعضاء الغير مسددين ومنع طباعة الكارنيه'
,
];
}
// ══════════════════════════════════════════════════════════════════════
// Group Discounts (المجمعة)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getGroupDiscount
(
int
$membershipCount
)
:
array
{
$override
=
RuleEngine
::
get
(
'membership.group_discount'
);
if
(
$override
!==
null
&&
isset
(
$override
[
'tiers'
]))
{
$tiers
=
$override
[
'tiers'
];
}
else
{
$tiers
=
[
[
'min'
=>
5
,
'max'
=>
10
,
'percentage'
=>
'3'
],
[
'min'
=>
11
,
'max'
=>
20
,
'percentage'
=>
'7'
],
[
'min'
=>
21
,
'max'
=>
null
,
'percentage'
=>
'10'
],
];
}
foreach
(
$tiers
as
$tier
)
{
$min
=
(
int
)
$tier
[
'min'
];
$max
=
$tier
[
'max'
]
!==
null
?
(
int
)
$tier
[
'max'
]
:
PHP_INT_MAX
;
if
(
$membershipCount
>=
$min
&&
$membershipCount
<=
$max
)
{
return
[
'eligible'
=>
true
,
'percentage'
=>
$tier
[
'percentage'
],
'count'
=>
$membershipCount
,
];
}
}
return
[
'eligible'
=>
false
,
'percentage'
=>
'0'
,
'count'
=>
$membershipCount
,
];
}
// ══════════════════════════════════════════════════════════════════════
// Carnet Rules (طباعة الكارنيهات)
// ══════════════════════════════════════════════════════════════════════
public
static
function
canPrintCarnet
(
int
$memberId
)
:
array
{
$db
=
App
::
getInstance
()
->
db
();
$currentYear
=
self
::
currentFinancialYear
();
// Check member status
$member
=
$db
->
selectOne
(
"SELECT status FROM members WHERE id = ? AND is_archived = 0"
,
[
$memberId
]
);
if
(
!
$member
)
{
return
[
'allowed'
=>
false
,
'reason'
=>
'العضو غير موجود'
];
}
if
(
in_array
(
$member
[
'status'
],
[
'suspended'
,
'frozen'
,
'dropped'
]))
{
return
[
'allowed'
=>
false
,
'reason'
=>
'العضوية غير فعالة (حالة: '
.
$member
[
'status'
]
.
')'
];
}
// Check annual subscription paid
$paid
=
$db
->
selectOne
(
"SELECT id FROM payments WHERE member_id = ? AND payment_type = 'annual_subscription' AND financial_year = ? AND is_voided = 0 LIMIT 1"
,
[
$memberId
,
$currentYear
]
);
if
(
!
$paid
)
{
return
[
'allowed'
=>
false
,
'reason'
=>
'لم يتم سداد الاشتراك السنوي للعام '
.
$currentYear
];
}
// Check outstanding installments
$unpaidInstallments
=
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM installment_payments WHERE member_id = ? AND status IN ('pending','overdue')"
,
[
$memberId
]
);
if
((
int
)
(
$unpaidInstallments
[
'cnt'
]
??
0
)
>
0
)
{
return
[
'allowed'
=>
false
,
'reason'
=>
'يوجد أقساط مستحقة غير مسددة'
];
}
return
[
'allowed'
=>
true
,
'reason'
=>
'مسموح بالطباعة'
];
}
public
static
function
getLostCarnetFee
()
:
array
{
$override
=
RuleEngine
::
get
(
'membership.lost_carnet_fee'
);
return
[
'fee'
=>
(
$override
[
'amount'
]
??
'200'
),
'description'
=>
'رسوم بدل فاقد كارنيه'
,
];
}
public
static
function
getCarnetPrintRules
()
:
array
{
return
[
'requires_annual_paid'
=>
true
,
'requires_no_installment_debt'
=>
true
,
'requires_active_status'
=>
true
,
'log_employee_and_date'
=>
true
,
'description'
=>
'لا يُسمح بطباعة الكارنيهات إلا بعد سداد الاشتراك السنوي — يظهر اسم الموظف والتاريخ'
,
];
}
// ══════════════════════════════════════════════════════════════════════
// Numbering (نظام ترقيم العضويات)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getNewNumberingFormat
()
:
array
{
$override
=
RuleEngine
::
get
(
'NEW_NUMBERING_START_DATE'
);
return
[
'format'
=>
'2/XXXX'
,
'start_from'
=>
(
$override
[
'start_number'
]
??
'1001'
),
'prefix'
=>
(
$override
[
'prefix'
]
??
'2'
),
'effective_date'
=>
(
$override
[
'effective_date'
]
??
'2026-07-01'
),
'applies_to'
=>
'new_memberships_only'
,
'description'
=>
'نظام ترقيم جديد متسلسل يبدأ من 2/1001 — يطبق على العضويات الجديدة فقط حتى 1/7/2026 ثم يعمم'
,
];
}
public
static
function
generateNextMembershipNumber
()
:
string
{
$db
=
App
::
getInstance
()
->
db
();
$config
=
self
::
getNewNumberingFormat
();
$prefix
=
$config
[
'prefix'
];
$last
=
$db
->
selectOne
(
"SELECT membership_number FROM members WHERE membership_number LIKE ? ORDER BY id DESC LIMIT 1"
,
[
$prefix
.
'/%'
]
);
if
(
$last
)
{
$parts
=
explode
(
'/'
,
$last
[
'membership_number'
]);
$nextNum
=
(
int
)
(
$parts
[
1
]
??
1000
)
+
1
;
}
else
{
$nextNum
=
(
int
)
$config
[
'start_from'
];
}
return
$prefix
.
'/'
.
$nextNum
;
}
// ══════════════════════════════════════════════════════════════════════
// Transfer/Separation Archive Rules (الأرشيف)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getArchiveRules
()
:
array
{
return
[
'never_delete'
=>
true
,
'archive_on_transfer'
=>
true
,
'archive_on_waiver'
=>
true
,
'archive_on_death_transfer'
=>
true
,
'archive_on_separation'
=>
true
,
'track_old_membership_number'
=>
true
,
'track_new_membership_number'
=>
true
,
'log_employee'
=>
true
,
'log_action_type'
=>
true
,
'log_timestamp'
=>
true
,
'log_notes'
=>
true
,
'description'
=>
'أي حذف أو تحويل أو تعديل لا يُحذف نهائياً — يتم نقل البيانات القديمة إلى الأرشيف مع الاحتفاظ بالتسلسل'
,
];
}
// ══════════════════════════════════════════════════════════════════════
// Board Authority (سلطة مجلس الأمناء)
// ══════════════════════════════════════════════════════════════════════
public
static
function
getBoardAuthorityRules
()
:
array
{
return
[
'can_adjust_subscription'
=>
true
,
'can_exempt_from_subscription'
=>
true
,
'can_adjust_membership_price'
=>
true
,
'can_offer_discounts'
=>
true
,
'can_set_installment_terms'
=>
true
,
'can_reinstate_dropped'
=>
true
,
'can_reduce_fine_on_review'
=>
true
,
'max_fine_on_reinstate'
=>
'10000'
,
'reference_article'
=>
'المادة 114'
,
];
}
// ══════════════════════════════════════════════════════════════════════
// Helpers
// ══════════════════════════════════════════════════════════════════════
private
static
function
currentFinancialYear
()
:
string
{
$month
=
(
int
)
date
(
'n'
);
$year
=
(
int
)
date
(
'Y'
);
if
(
$month
>=
7
)
{
return
$year
.
'/'
.
(
$year
+
1
);
}
return
(
$year
-
1
)
.
'/'
.
$year
;
}
}
app/Modules/Members/Services/WaiverService.php
0 → 100644
View file @
2b105af3
<?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
(),
];
}
}
app/Modules/Subscriptions/Services/SubscriptionGenerator.php
View file @
2b105af3
...
...
@@ -16,14 +16,27 @@ final class SubscriptionGenerator
$ts
=
date
(
'Y-m-d H:i:s'
);
$empId
=
$employee
?
(
int
)
$employee
->
id
:
null
;
// Get rates from service catalog
$memberRate
=
self
::
getRate
(
'SVC_ANNUAL_MEMBER'
);
$spouseRate
=
self
::
getRate
(
'SVC_ANNUAL_SPOUSE'
);
$childRate
=
self
::
getRate
(
'SVC_ANNUAL_CHILD'
);
$tempRate
=
self
::
getRate
(
'SVC_ANNUAL_TEMP'
);
// Get year-specific rates (e.g. 2024/2025 -> suffix 2024)
$yearParts
=
explode
(
'/'
,
$financialYear
);
$yearSuffix
=
$yearParts
[
0
]
??
''
;
$memberRate
=
self
::
getRate
(
'SVC_ANNUAL_MEMBER_'
.
$yearSuffix
)
?:
self
::
getRate
(
'SVC_ANNUAL_MEMBER'
);
$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'
);
$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
$memberWhere
=
"m.status = 'active' AND m.is_archived = 0 AND m.membership_type NOT IN ('honorary')"
;
$memberParams
=
[];
...
...
app/Modules/Transfers/Services/SeparationFeeCalculator.php
View file @
2b105af3
...
...
@@ -78,6 +78,31 @@ final class SeparationFeeCalculator
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
{
$ruleMap
=
[
...
...
app/Modules/Transfers/Services/TransferEligibility.php
0 → 100644
View file @
2b105af3
<?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
;
}
}
database/seeds/Phase_46_001_seed_subscription_rates_and_prices.php
0 → 100644
View file @
2b105af3
<?php
declare
(
strict_types
=
1
);
/**
* Phase 46: Subscription Rates & Membership Prices
*
* Seeds annual subscription rates per financial year (2023–2025),
* current membership prices (with historical entries), and
* the new numbering system business rule.
*/
return
function
(
\App\Core\Database
$db
)
{
$now
=
date
(
'Y-m-d H:i:s'
);
// ─── Check service_catalog table exists ────────────────────────────────────
$tableExists
=
$db
->
selectOne
(
"SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'service_catalog'"
);
if
(
!
$tableExists
)
{
echo
" [SKIP] service_catalog table does not exist.
\n
"
;
return
;
}
// ─── 1. Annual subscription rates per financial year ────────────────────────
$annualRates
=
[
// FY 2023 (2023-07-01 to 2024-06-30)
[
'SVC_ANNUAL_MEMBER_2023'
,
'اشتراك سنوي - عضو 2023'
,
'Annual Subscription - Member 2023'
,
410.00
,
'2023-07-01'
,
'2024-06-30'
],
[
'SVC_ANNUAL_SPOUSE_2023'
,
'اشتراك سنوي - زوج/ة 2023'
,
'Annual Subscription - Spouse 2023'
,
410.00
,
'2023-07-01'
,
'2024-06-30'
],
[
'SVC_ANNUAL_CHILD_2023'
,
'اشتراك سنوي - ابن 2023'
,
'Annual Subscription - Child 2023'
,
185.00
,
'2023-07-01'
,
'2024-06-30'
],
[
'SVC_ANNUAL_TEMP_2023'
,
'اشتراك سنوي - مؤقت 2023'
,
'Annual Subscription - Temp 2023'
,
185.00
,
'2023-07-01'
,
'2024-06-30'
],
// FY 2024 (2024-07-01 to 2025-06-30)
[
'SVC_ANNUAL_MEMBER_2024'
,
'اشتراك سنوي - عضو 2024'
,
'Annual Subscription - Member 2024'
,
410.00
,
'2024-07-01'
,
'2025-06-30'
],
[
'SVC_ANNUAL_SPOUSE_2024'
,
'اشتراك سنوي - زوج/ة 2024'
,
'Annual Subscription - Spouse 2024'
,
410.00
,
'2024-07-01'
,
'2025-06-30'
],
[
'SVC_ANNUAL_CHILD_2024'
,
'اشتراك سنوي - ابن 2024'
,
'Annual Subscription - Child 2024'
,
185.00
,
'2024-07-01'
,
'2025-06-30'
],
[
'SVC_ANNUAL_TEMP_2024'
,
'اشتراك سنوي - مؤقت 2024'
,
'Annual Subscription - Temp 2024'
,
185.00
,
'2024-07-01'
,
'2025-06-30'
],
// FY 2025 (2025-07-01 to 2026-06-30) — 20% increase
[
'SVC_ANNUAL_MEMBER_2025'
,
'اشتراك سنوي - عضو 2025'
,
'Annual Subscription - Member 2025'
,
492.00
,
'2025-07-01'
,
'2026-06-30'
],
[
'SVC_ANNUAL_SPOUSE_2025'
,
'اشتراك سنوي - زوج/ة 2025'
,
'Annual Subscription - Spouse 2025'
,
492.00
,
'2025-07-01'
,
'2026-06-30'
],
[
'SVC_ANNUAL_CHILD_2025'
,
'اشتراك سنوي - ابن 2025'
,
'Annual Subscription - Child 2025'
,
222.00
,
'2025-07-01'
,
'2026-06-30'
],
[
'SVC_ANNUAL_TEMP_2025'
,
'اشتراك سنوي - مؤقت 2025'
,
'Annual Subscription - Temp 2025'
,
222.00
,
'2025-07-01'
,
'2026-06-30'
],
// General fallback rates (no effective_to)
[
'SVC_ANNUAL_MEMBER'
,
'اشتراك سنوي - عضو'
,
'Annual Subscription - Member'
,
492.00
,
'2025-07-01'
,
null
],
[
'SVC_ANNUAL_SPOUSE'
,
'اشتراك سنوي - زوج/ة'
,
'Annual Subscription - Spouse'
,
492.00
,
'2025-07-01'
,
null
],
[
'SVC_ANNUAL_CHILD'
,
'اشتراك سنوي - ابن'
,
'Annual Subscription - Child'
,
222.00
,
'2025-07-01'
,
null
],
[
'SVC_ANNUAL_TEMP'
,
'اشتراك سنوي - مؤقت'
,
'Annual Subscription - Temp'
,
222.00
,
'2025-07-01'
,
null
],
];
foreach
(
$annualRates
as
[
$code
,
$nameAr
,
$nameEn
,
$amount
,
$from
,
$to
])
{
$db
->
query
(
"INSERT INTO service_catalog (service_code, name_ar, name_en, category, base_amount, branch_id, effective_from, effective_to, is_active, created_at, updated_at)
VALUES (?, ?, ?, 'subscription', ?, NULL, ?, ?, 1, ?, ?)
ON DUPLICATE KEY UPDATE
name_ar = VALUES(name_ar),
name_en = VALUES(name_en),
base_amount = VALUES(base_amount),
effective_to = VALUES(effective_to),
is_active = 1,
updated_at = VALUES(updated_at)"
,
[
$code
,
$nameAr
,
$nameEn
,
$amount
,
$from
,
$to
,
$now
,
$now
]
);
}
echo
" [OK] Annual subscription rates seeded (2023–2025 + fallback).
\n
"
;
// ─── 2. Membership prices ───────────────────────────────────────────────────
$membershipPrices
=
[
// Current prices (effective 2024-07-01, no end date)
[
'SVC_MEMBERSHIP_HIGH_QUAL'
,
'رسوم عضوية - مؤهل عالي'
,
'Membership Fee - High Qualification'
,
150000.00
,
'2024-07-01'
,
null
],
[
'SVC_MEMBERSHIP_MED_QUAL'
,
'رسوم عضوية - مؤهل متوسط'
,
'Membership Fee - Medium Qualification'
,
225000.00
,
'2024-07-01'
,
null
],
[
'SVC_MEMBERSHIP_NO_QUAL'
,
'رسوم عضوية - بدون مؤهل'
,
'Membership Fee - No Qualification'
,
300000.00
,
'2024-07-01'
,
null
],
// Historical prices (before 2024-07-01)
[
'SVC_MEMBERSHIP_HIGH_QUAL_OLD'
,
'رسوم عضوية - مؤهل عالي (قديم)'
,
'Membership Fee - High Qualification (Old)'
,
114000.00
,
'2023-01-01'
,
'2024-06-30'
],
[
'SVC_MEMBERSHIP_MED_QUAL_OLD'
,
'رسوم عضوية - مؤهل متوسط (قديم)'
,
'Membership Fee - Medium Qualification (Old)'
,
171000.00
,
'2023-01-01'
,
'2024-06-30'
],
[
'SVC_MEMBERSHIP_NO_QUAL_OLD'
,
'رسوم عضوية - بدون مؤهل (قديم)'
,
'Membership Fee - No Qualification (Old)'
,
228000.00
,
'2023-01-01'
,
'2024-06-30'
],
];
foreach
(
$membershipPrices
as
[
$code
,
$nameAr
,
$nameEn
,
$amount
,
$from
,
$to
])
{
$db
->
query
(
"INSERT INTO service_catalog (service_code, name_ar, name_en, category, base_amount, branch_id, effective_from, effective_to, is_active, created_at, updated_at)
VALUES (?, ?, ?, 'membership', ?, NULL, ?, ?, 1, ?, ?)
ON DUPLICATE KEY UPDATE
name_ar = VALUES(name_ar),
name_en = VALUES(name_en),
base_amount = VALUES(base_amount),
effective_to = VALUES(effective_to),
is_active = 1,
updated_at = VALUES(updated_at)"
,
[
$code
,
$nameAr
,
$nameEn
,
$amount
,
$from
,
$to
,
$now
,
$now
]
);
}
echo
" [OK] Membership prices seeded (current + historical).
\n
"
;
// ─── 3. Business rule: new numbering system ─────────────────────────────────
$ruleTableExists
=
$db
->
selectOne
(
"SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'business_rules'"
);
if
(
!
$ruleTableExists
)
{
echo
" [SKIP] business_rules table does not exist.
\n
"
;
return
;
}
$ruleCode
=
'NEW_NUMBERING_START_DATE'
;
$ruleValue
=
json_encode
([
'date'
=>
'2026-07-01'
,
'format'
=>
'2/{seq}'
,
'start_seq'
=>
1001
],
JSON_UNESCAPED_UNICODE
);
$existingRule
=
$db
->
selectOne
(
"SELECT id FROM business_rules WHERE rule_code = ? AND branch_id IS NULL"
,
[
$ruleCode
]
);
if
(
$existingRule
)
{
$db
->
update
(
'business_rules'
,
[
'current_value_json'
=>
$ruleValue
,
'updated_at'
=>
$now
,
],
'id = ?'
,
[(
int
)
$existingRule
[
'id'
]]);
}
else
{
$db
->
insert
(
'business_rules'
,
[
'rule_code'
=>
$ruleCode
,
'category'
=>
'workflow'
,
'name_ar'
=>
'تاريخ بدء نظام الترقيم الجديد'
,
'name_en'
=>
'New Numbering System Start Date'
,
'description_ar'
=>
'يحدد تاريخ بدء نظام الترقيم الجديد وصيغة الرقم ورقم البداية'
,
'description_en'
=>
'Defines the start date, format, and starting sequence for the new numbering system'
,
'parameters_json'
=>
null
,
'current_value_json'
=>
$ruleValue
,
'data_type'
=>
'json'
,
'branch_id'
=>
null
,
'effective_from'
=>
'2026-07-01'
,
'effective_to'
=>
null
,
'is_active'
=>
1
,
'version'
=>
1
,
'created_at'
=>
$now
,
'updated_at'
=>
$now
,
'created_by'
=>
null
,
'updated_by'
=>
null
,
]);
}
echo
" [OK] Business rule NEW_NUMBERING_START_DATE seeded.
\n
"
;
// ─── 4. Separation fee tiers (رسوم الفصل) ─────────────────────────────────
$separationRules
=
[
[
'SEPARATION_FEE_YEAR_1'
,
'رسم فصل - السنة الأولى'
,
'{"percentage":"30"}'
],
[
'SEPARATION_FEE_YEAR_2'
,
'رسم فصل - السنة الثانية'
,
'{"percentage":"20"}'
],
[
'SEPARATION_FEE_YEAR_3'
,
'رسم فصل - السنة الثالثة'
,
'{"percentage":"15"}'
],
[
'SEPARATION_FEE_YEAR_4'
,
'رسم فصل - السنة الرابعة'
,
'{"percentage":"10"}'
],
[
'SEPARATION_FEE_YEAR_5'
,
'رسم فصل - السنة الخامسة'
,
'{"percentage":"5"}'
],
[
'SEPARATION_FEE_YEAR_6_PLUS'
,
'رسم فصل - السنة السادسة فما فوق'
,
'{"percentage":"2.5"}'
],
];
foreach
(
$separationRules
as
[
$code
,
$nameAr
,
$value
])
{
$existing
=
$db
->
selectOne
(
"SELECT id FROM business_rules WHERE rule_code = ? AND branch_id IS NULL"
,
[
$code
]);
if
(
!
$existing
)
{
$db
->
insert
(
'business_rules'
,
[
'rule_code'
=>
$code
,
'category'
=>
'transfer'
,
'name_ar'
=>
$nameAr
,
'name_en'
=>
'Separation Fee - '
.
$code
,
'description_ar'
=>
'نسبة رسم الفصل من قيمة العضوية الجديدة'
,
'description_en'
=>
'Separation fee percentage from current membership value'
,
'parameters_json'
=>
null
,
'current_value_json'
=>
$value
,
'data_type'
=>
'json'
,
'branch_id'
=>
null
,
'effective_from'
=>
'2024-07-01'
,
'effective_to'
=>
null
,
'is_active'
=>
1
,
'version'
=>
1
,
'created_at'
=>
$now
,
'updated_at'
=>
$now
,
'created_by'
=>
null
,
'updated_by'
=>
null
,
]);
}
}
echo
" [OK] Separation fee tiers seeded (year 1-6+).
\n
"
;
// ─── 5. Form fees & key business rules ────────────────────────────────────
$miscRules
=
[
[
'FORM_NEW_MEMBERSHIP_FEE'
,
'رسم استمارة عضوية جديدة'
,
'{"form_amount":"500","martyrs_stamp":"5","total":"505"}'
,
'membership'
],
[
'FORM_ADDITION_FEE'
,
'رسم استمارة إضافة'
,
'{"amount":"570"}'
,
'membership'
],
[
'FORM_TRANSFER_FEE'
,
'رسم استمارة فصل/تحويل'
,
'{"amount":"570"}'
,
'transfer'
],
[
'TEMPORARY_MEMBER_FEE'
,
'رسم العضو المؤقت'
,
'{"percentage":"10"}'
,
'membership'
],
[
'NANNY_FEE'
,
'رسم المربية'
,
'{"percentage":"5"}'
,
'membership'
],
[
'WAIVER_FEE'
,
'رسم التنازل عن العضوية'
,
'{"percentage":"30"}'
,
'transfer'
],
[
'ATHLETIC_CONVERSION_FEE'
,
'رسم تحويل العضوية الرياضية'
,
'{"percentage":"50","min_years":8}'
,
'transfer'
],
[
'FOREIGN_MEMBER_FEE'
,
'رسم العضو الأجنبي'
,
'{"fee_usd":"10000","currency":"USD"}'
,
'membership'
],
[
'MALE_CHILD_FREEZE_AGE'
,
'سن تجميد عضوية الابن'
,
'{"value":25}'
,
'workflow'
],
[
'DIVORCE_REQUEST_WINDOW'
,
'مهلة طلب فصل العضوية بعد الطلاق'
,
'{"max_years":1}'
,
'transfer'
],
[
'DIVORCE_MIN_MEMBERSHIP_YEARS'
,
'حد أدنى سنوات عضوية للطلاق'
,
'{"min_years":5,"waived_if_children":true}'
,
'transfer'
],
[
'DIVORCE_SAME_FORM_FEE'
,
'رسم فصل زوج/ة بنفس الاستمارة'
,
'{"percentage":"10"}'
,
'transfer'
],
[
'DIVORCE_JOINED_AFTER_FEE'
,
'رسم فصل زوج/ة منضم بعد العضوية'
,
'{"percentage":"50"}'
,
'transfer'
],
[
'FINE_ACCUMULATION_MAX_YEARS'
,
'حد أقصى تراكم الغرامات'
,
'{"max_years":5,"drop_after":true}'
,
'penalties'
],
[
'MEMBERSHIP_DROP_UNPAID_YEARS'
,
'إسقاط العضوية لعدم السداد'
,
'{"years":5}'
,
'penalties'
],
[
'REINSTATEMENT_WINDOW'
,
'مهلة إعادة العضوية بعد الإسقاط'
,
'{"max_years":1}'
,
'workflow'
],
[
'DEVELOPMENT_FEE'
,
'رسوم تنمية'
,
'{"amount":"35"}'
,
'subscription'
],
];
foreach
(
$miscRules
as
[
$code
,
$nameAr
,
$value
,
$category
])
{
$existing
=
$db
->
selectOne
(
"SELECT id FROM business_rules WHERE rule_code = ? AND branch_id IS NULL"
,
[
$code
]);
if
(
!
$existing
)
{
$db
->
insert
(
'business_rules'
,
[
'rule_code'
=>
$code
,
'category'
=>
$category
,
'name_ar'
=>
$nameAr
,
'name_en'
=>
$code
,
'description_ar'
=>
$nameAr
,
'description_en'
=>
$code
,
'parameters_json'
=>
null
,
'current_value_json'
=>
$value
,
'data_type'
=>
'json'
,
'branch_id'
=>
null
,
'effective_from'
=>
'2024-07-01'
,
'effective_to'
=>
null
,
'is_active'
=>
1
,
'version'
=>
1
,
'created_at'
=>
$now
,
'updated_at'
=>
$now
,
'created_by'
=>
null
,
'updated_by'
=>
null
,
]);
}
}
echo
" [OK] All business rules seeded (forms, fees, transfers, penalties).
\n
"
;
// ─── 6. Subscription year adjustment rules ────────────────────────────────
$yearAdjustments
=
[
[
'SUBSCRIPTION_YEAR_ADJUSTMENT_2023'
,
'تعديل اشتراك 2023 - خصم 50%'
,
'{"discount_percentage":"50","note":"خصم 50% مطبق"}'
],
[
'SUBSCRIPTION_YEAR_ADJUSTMENT_2025'
,
'تعديل اشتراك 2025 - زيادة 20%'
,
'{"increase_percentage":"20","note":"زيادة 20% عن السنة السابقة"}'
],
];
foreach
(
$yearAdjustments
as
[
$code
,
$nameAr
,
$value
])
{
$existing
=
$db
->
selectOne
(
"SELECT id FROM business_rules WHERE rule_code = ? AND branch_id IS NULL"
,
[
$code
]);
if
(
!
$existing
)
{
$db
->
insert
(
'business_rules'
,
[
'rule_code'
=>
$code
,
'category'
=>
'subscription'
,
'name_ar'
=>
$nameAr
,
'name_en'
=>
$code
,
'description_ar'
=>
$nameAr
,
'description_en'
=>
'Year-specific subscription adjustment'
,
'parameters_json'
=>
null
,
'current_value_json'
=>
$value
,
'data_type'
=>
'json'
,
'branch_id'
=>
null
,
'effective_from'
=>
'2024-07-01'
,
'effective_to'
=>
null
,
'is_active'
=>
1
,
'version'
=>
1
,
'created_at'
=>
$now
,
'updated_at'
=>
$now
,
'created_by'
=>
null
,
'updated_by'
=>
null
,
]);
}
}
echo
" [OK] Subscription year adjustments seeded (2023 discount, 2025 increase).
\n
"
;
// ─── 7. Acquired member children fee tiers (أبناء العضو المكتسب) ──────────
$acquiredChildRules
=
[
[
'DIVORCE_CHILD_UNDER_12'
,
'رسم ابن مكتسب عضوية - أقل من 12'
,
'{"percentage":"15"}'
],
[
'DIVORCE_CHILD_12_TO_16'
,
'رسم ابن مكتسب عضوية - 12 إلى 16'
,
'{"percentage":"20"}'
],
[
'DIVORCE_CHILD_16_TO_18'
,
'رسم ابن مكتسب عضوية - 16 إلى 18'
,
'{"percentage":"25"}'
],
[
'DIVORCE_CHILD_OVER_18'
,
'رسم ابن مكتسب عضوية - فوق 18'
,
'{"percentage":"30"}'
],
];
foreach
(
$acquiredChildRules
as
[
$code
,
$nameAr
,
$value
])
{
$existing
=
$db
->
selectOne
(
"SELECT id FROM business_rules WHERE rule_code = ? AND branch_id IS NULL"
,
[
$code
]);
if
(
!
$existing
)
{
$db
->
insert
(
'business_rules'
,
[
'rule_code'
=>
$code
,
'category'
=>
'transfer'
,
'name_ar'
=>
$nameAr
,
'name_en'
=>
$code
,
'description_ar'
=>
'نسبة رسوم إضافة أبناء العضو المكتسب بعد الانفصال'
,
'description_en'
=>
'Fee percentage for acquired member children after separation'
,
'parameters_json'
=>
null
,
'current_value_json'
=>
$value
,
'data_type'
=>
'json'
,
'branch_id'
=>
null
,
'effective_from'
=>
'2024-07-01'
,
'effective_to'
=>
null
,
'is_active'
=>
1
,
'version'
=>
1
,
'created_at'
=>
$now
,
'updated_at'
=>
$now
,
'created_by'
=>
null
,
'updated_by'
=>
null
,
]);
}
}
echo
" [OK] Acquired member children fee tiers seeded.
\n
"
;
};
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment