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