Commit 6873f19e authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix fee calculations: use current pricing, remove dev fee for dependents

Business rule fixes per ticket #68:

1. Spouse/child fee percentages now use CURRENT membership value from
   pricing_configs (150k/225k/300k) instead of old stored value at creation

2. Spouse annual subscription = 492 (SVC_ANNUAL_SPOUSE) WITHOUT dev fee
   Was incorrectly using child rate (222) + dev fee (35) = 257

3. Child/temp annual subscription = 222 WITHOUT dev fee
   Was incorrectly adding 35 dev fee = 257

4. Late marriage penalty: now calculated from LATER of (marriage_date,
   member_activated_at) to TODAY — not from marriage to member creation

5. SubscriptionGenerator batch: spouse/child/temp subscriptions no longer
   include development_fee (set to 0.00). Only member keeps dev fee.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent e1f1837a
......@@ -23,14 +23,13 @@ final class ChildFeeCalculator
$membershipValue = $member['membership_value'] ?? '0.00';
$isOnInitialForm = FormFeeService::isOnInitialForm($member);
if (bccomp($membershipValue, '0.01', 2) < 0 && !empty($member['qualification_id']) && !empty($member['branch_id'])) {
$pricing = $db->selectOne(
"SELECT price FROM pricing_configs WHERE branch_id = ? AND qualification_id = ? AND membership_type = 'working' AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY effective_from DESC LIMIT 1",
[(int) $member['branch_id'], (int) $member['qualification_id']]
// Always use current membership value from pricing_configs
$currentPricing = $db->selectOne(
"SELECT price FROM pricing_configs WHERE branch_id = ? AND qualification_id = ? AND membership_type = ? AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY effective_from DESC LIMIT 1",
[(int) $member['branch_id'], (int) ($member['qualification_id'] ?? 0), $member['membership_type'] ?? 'working']
);
if ($pricing && bccomp($pricing['price'], '0.01', 2) >= 0) {
$membershipValue = $pricing['price'];
}
if ($currentPricing && bccomp($currentPricing['price'], '0.01', 2) >= 0) {
$membershipValue = $currentPricing['price'];
}
if (bccomp($membershipValue, '0.00', 2) <= 0 && !$isOnInitialForm) {
......@@ -112,8 +111,7 @@ final class ChildFeeCalculator
$breakdown[] = '📝 رسوم استمارة إضافة: ' . money($formFeeOnly);
}
if (bccomp($annualSubscription, '0', 2) > 0) {
$devFee = RuleEngine::get('DEVELOPMENT_FEE')['amount'] ?? '35.00';
$breakdown[] = '📅 اشتراك سنوي: ' . money(bcsub($annualSubscription, $devFee, 2)) . ' + تنمية: ' . money($devFee) . ' = ' . money($annualSubscription);
$breakdown[] = '📅 اشتراك سنوي: ' . money($annualSubscription);
}
} else {
if (bccomp($childFee, '0', 2) > 0) {
......@@ -123,8 +121,7 @@ final class ChildFeeCalculator
$breakdown[] = '📝 رسوم استمارة إضافة: ' . money($formFeeOnly);
}
if (bccomp($annualSubscription, '0', 2) > 0) {
$devFee = RuleEngine::get('DEVELOPMENT_FEE')['amount'] ?? '35.00';
$breakdown[] = '📅 اشتراك سنوي: ' . money(bcsub($annualSubscription, $devFee, 2)) . ' + تنمية: ' . money($devFee) . ' = ' . money($annualSubscription);
$breakdown[] = '📅 اشتراك سنوي: ' . money($annualSubscription);
}
}
$breakdown[] = '═══════════════════════════';
......
......@@ -72,12 +72,8 @@ final class FormFeeService
$childRate = $db->selectOne(
"SELECT base_amount FROM service_catalog WHERE service_code LIKE 'SVC_ANNUAL_CHILD%' AND is_active = 1 ORDER BY effective_from DESC LIMIT 1"
);
$devFeeData = RuleEngine::get('DEVELOPMENT_FEE');
$rate = $childRate ? $childRate['base_amount'] : '222.00';
$dev = $devFeeData['amount'] ?? '35.00';
return bcadd($rate, $dev, 2);
return $childRate ? $childRate['base_amount'] : '222.00';
}
/**
......
......@@ -35,15 +35,13 @@ final class SpouseFeeCalculator
$membershipValue = $member['membership_value'] ?? '0.00';
$isOnInitialForm = FormFeeService::isOnInitialForm($member);
// Resolve membership value from pricing_configs if not stored yet
if (bccomp($membershipValue, '0.01', 2) < 0 && !empty($member['qualification_id']) && !empty($member['branch_id'])) {
$pricing = $db->selectOne(
"SELECT price FROM pricing_configs WHERE branch_id = ? AND qualification_id = ? AND membership_type = 'working' AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY effective_from DESC LIMIT 1",
[(int) $member['branch_id'], (int) $member['qualification_id']]
// Always use current membership value from pricing_configs
$currentPricing = $db->selectOne(
"SELECT price FROM pricing_configs WHERE branch_id = ? AND qualification_id = ? AND membership_type = ? AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY effective_from DESC LIMIT 1",
[(int) $member['branch_id'], (int) ($member['qualification_id'] ?? 0), $member['membership_type'] ?? 'working']
);
if ($pricing && bccomp($pricing['price'], '0.01', 2) >= 0) {
$membershipValue = $pricing['price'];
}
if ($currentPricing && bccomp($currentPricing['price'], '0.01', 2) >= 0) {
$membershipValue = $currentPricing['price'];
}
if (bccomp($membershipValue, '0.01', 2) < 0 && !$isOnInitialForm) {
......@@ -82,9 +80,13 @@ final class SpouseFeeCalculator
}
$formFeeOnly = FormFeeService::getFormFeeOnly($memberId, $member);
$annualSubscription = !$isOnInitialForm
? FormFeeService::getAnnualSubscriptionForAddition()
: '0.00';
$annualSubscription = '0.00';
if (!$isOnInitialForm) {
$spouseSubRate = $db->selectOne(
"SELECT base_amount FROM service_catalog WHERE service_code LIKE 'SVC_ANNUAL_SPOUSE%' AND is_active = 1 ORDER BY effective_from DESC LIMIT 1"
);
$annualSubscription = $spouseSubRate ? $spouseSubRate['base_amount'] : '492.00';
}
$formFee = bcadd($formFeeOnly, $annualSubscription, 2);
$isAcquiredMember = self::isAcquiredMember($memberId);
......@@ -94,6 +96,7 @@ final class SpouseFeeCalculator
$marriageDate = $spouseData['marriage_date'] ?? null;
$memberCreatedDate = substr($member['created_at'] ?? date('Y-m-d'), 0, 10);
$memberActivatedDate = !empty($member['activated_at']) ? substr($member['activated_at'], 0, 10) : $memberCreatedDate;
$percentage = '0.00';
$percentageFee = '0.00';
......@@ -136,7 +139,7 @@ final class SpouseFeeCalculator
$ruleApplied = 'الزوجة الثانية — ' . $percentage . '% من قيمة العضوية (بدون غرامات — استمارة أولى)';
} else {
$ruleApplied = 'الزوجة الثانية — ' . $percentage . '% من قيمة العضوية + ' . $annualPerYear . ' ج.م عن كل سنة';
$yearCount = self::calculateYears($marriageDate, $memberCreatedDate);
$yearCount = self::calculateYears($marriageDate, $memberActivatedDate);
$yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2);
}
break;
......@@ -150,7 +153,7 @@ final class SpouseFeeCalculator
$ruleApplied = 'الزوجة الثالثة — ' . $percentage . '% من قيمة العضوية (بدون غرامات — استمارة أولى)';
} else {
$ruleApplied = 'الزوجة الثالثة — ' . $percentage . '% من قيمة العضوية + ' . $annualPerYear . ' ج.م عن كل سنة';
$yearCount = self::calculateYears($marriageDate, $memberCreatedDate);
$yearCount = self::calculateYears($marriageDate, $memberActivatedDate);
$yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2);
}
break;
......@@ -164,7 +167,7 @@ final class SpouseFeeCalculator
$ruleApplied = 'الزوجة الرابعة — ' . $percentage . '% من قيمة العضوية (بدون غرامات — استمارة أولى)';
} else {
$ruleApplied = 'الزوجة الرابعة — ' . $percentage . '% من قيمة العضوية + ' . $annualPerYear . ' ج.م عن كل سنة';
$yearCount = self::calculateYears($marriageDate, $memberCreatedDate);
$yearCount = self::calculateYears($marriageDate, $memberActivatedDate);
$yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2);
}
break;
......@@ -206,35 +209,33 @@ final class SpouseFeeCalculator
];
}
private static function calculateYears(?string $marriageDate, string $memberCreatedDate): int
private static function calculateYears(?string $marriageDate, string $memberActivatedDate): int
{
if (!$marriageDate) {
return 0;
}
$marriageTs = strtotime($marriageDate);
$memberTs = strtotime($memberCreatedDate);
$memberTs = strtotime($memberActivatedDate);
if (!$marriageTs || !$memberTs) {
return 0;
}
// Annual fine only applies if marriage is BEFORE membership acquisition
if ($marriageTs >= $memberTs) {
return 0;
}
$start = new \DateTime(date('Y-m-d', $marriageTs));
$end = new \DateTime(date('Y-m-d', $memberTs));
// Start counting from LATER of marriage or membership activation
$startTs = max($marriageTs, $memberTs);
$start = new \DateTime(date('Y-m-d', $startTs));
$end = new \DateTime(); // today
$diff = $end->diff($start);
$years = $diff->y;
// Partial year counts as full year
if ($diff->m > 0 || $diff->d > 0) {
$years++;
}
return max(1, $years);
return max(0, $years);
}
private static function isAcquiredMember(int $memberId): bool
......@@ -281,23 +282,21 @@ final class SpouseFeeCalculator
$lines[] = '📝 رسوم استمارة إضافة: ' . money($formFee);
}
if (bccomp($annualSubscription, '0', 2) > 0) {
$devFee = RuleEngine::get('DEVELOPMENT_FEE')['amount'] ?? '35.00';
$lines[] = '📅 اشتراك سنوي: ' . money(bcsub($annualSubscription, $devFee, 2)) . ' + تنمية: ' . money($devFee) . ' = ' . money($annualSubscription);
$lines[] = '📅 اشتراك سنوي: ' . money($annualSubscription);
}
} else {
if (bccomp($pctFee, '0', 2) > 0) {
$lines[] = "📊 نسبة {$pct}% × " . money($membershipValue) . ' = ' . money($pctFee);
}
if (bccomp($annual, '0', 2) > 0 && $years > 0) {
$lines[] = "📅 غرامة زواج سابق للعضوية: {$annual} ج.م × {$years} سنة = " . money($yearlyTotal);
$lines[] = ' (تحسب من تاريخ الزواج حتى تاريخ اكتساب العضوية — كسر السنة سنة كاملة)';
$lines[] = "📅 غرامة تأخير إضافة: {$annual} ج.م × {$years} سنة = " . money($yearlyTotal);
$lines[] = ' (تحسب من الأحدث بين تاريخ الزواج أو العضوية حتى اليوم — كسر السنة سنة كاملة)';
}
if (bccomp($formFee, '0', 2) > 0) {
$lines[] = '📝 رسوم استمارة إضافة: ' . money($formFee);
}
if (bccomp($annualSubscription, '0', 2) > 0) {
$devFee = RuleEngine::get('DEVELOPMENT_FEE')['amount'] ?? '35.00';
$lines[] = '📅 اشتراك سنوي: ' . money(bcsub($annualSubscription, $devFee, 2)) . ' + تنمية: ' . money($devFee) . ' = ' . money($annualSubscription);
$lines[] = '📅 اشتراك سنوي: ' . money($annualSubscription);
}
}
......
......@@ -78,11 +78,10 @@ final class SubscriptionGenerator
]);
$created++;
// Spouses
// Spouses — NO dev fee
if ($db->tableExists('spouses')) {
$spouses = $db->select("SELECT id, full_name_ar FROM spouses WHERE member_id = ? AND is_archived = 0 AND status = 'active'", [$memberId]);
foreach ($spouses as $sp) {
$spTotal = bcadd($spouseRate, $devFee, 2);
$db->insert('subscriptions', [
'member_id' => $memberId,
'financial_year' => $financialYear,
......@@ -90,8 +89,8 @@ final class SubscriptionGenerator
'person_id' => (int) $sp['id'],
'person_name' => $sp['full_name_ar'],
'base_amount' => $spouseRate,
'development_fee' => $devFee,
'total_amount' => $spTotal,
'development_fee' => '0.00',
'total_amount' => $spouseRate,
'status' => 'pending',
'created_at' => $ts,
'updated_at' => $ts,
......@@ -101,11 +100,10 @@ final class SubscriptionGenerator
}
}
// Children
// Children — NO dev fee
if ($db->tableExists('children')) {
$children = $db->select("SELECT id, full_name_ar FROM children WHERE member_id = ? AND is_archived = 0 AND status = 'active'", [$memberId]);
foreach ($children as $ch) {
$chTotal = bcadd($childRate, $devFee, 2);
$db->insert('subscriptions', [
'member_id' => $memberId,
'financial_year' => $financialYear,
......@@ -113,8 +111,8 @@ final class SubscriptionGenerator
'person_id' => (int) $ch['id'],
'person_name' => $ch['full_name_ar'],
'base_amount' => $childRate,
'development_fee' => $devFee,
'total_amount' => $chTotal,
'development_fee' => '0.00',
'total_amount' => $childRate,
'status' => 'pending',
'created_at' => $ts,
'updated_at' => $ts,
......@@ -124,11 +122,10 @@ final class SubscriptionGenerator
}
}
// Temporary members
// Temporary members — NO dev fee
if ($db->tableExists('temporary_members')) {
$temps = $db->select("SELECT id, full_name_ar FROM temporary_members WHERE member_id = ? AND is_archived = 0 AND status = 'active'", [$memberId]);
foreach ($temps as $t) {
$tTotal = bcadd($tempRate, $devFee, 2);
$db->insert('subscriptions', [
'member_id' => $memberId,
'financial_year' => $financialYear,
......@@ -136,8 +133,8 @@ final class SubscriptionGenerator
'person_id' => (int) $t['id'],
'person_name' => $t['full_name_ar'],
'base_amount' => $tempRate,
'development_fee' => $devFee,
'total_amount' => $tTotal,
'development_fee' => '0.00',
'total_amount' => $tempRate,
'status' => 'pending',
'created_at' => $ts,
'updated_at' => $ts,
......
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