Commit 50a1481e authored by Administrator's avatar Administrator

Update 1 files via Son of Anton

parent ca7312ce
...@@ -7,141 +7,143 @@ use App\Core\App; ...@@ -7,141 +7,143 @@ use App\Core\App;
use App\Modules\Spouses\Models\Spouse; use App\Modules\Spouses\Models\Spouse;
/** /**
* Spouse Fee Calculator — implements ALL fee rules from club regulations. * Spouse Fee Calculator
* *
* Fee Structure: * RULES FROM CLUB REGULATIONS:
* ══════════════════════════════════════════════════════════════════ * ═══════════════════════════════════════════════════════════════
* 1st Spouse (during creation): FREE — included in base membership value * 1st Spouse (same form / initial creation): FREE (included in membership value)
* 1st Spouse (added later, basis member): 15% of membership value + 570 form * 1st Spouse (added later, أساسي basis): 15% of membership value + 570 form
* 1st Spouse (added later, acquired member): 50% of membership value + 570 form * 1st Spouse (added later, مكتسب acquired): 50% of membership value + 570 form
* Foreign Spouse: 15% of membership value * Foreign Spouse (1st): 15% of membership value
* 2nd Spouse: 10% + 150 EGP/year (from marriage or acquisition, whichever later) * 2nd Spouse: 10% of membership value + 150 EGP × years
* 3rd Spouse: 20% + 200 EGP/year (from marriage or acquisition, whichever later) * 3rd Spouse: 20% of membership value + 200 EGP × years
* 4th Spouse: 30% + 300 EGP/year (from marriage or acquisition, whichever later) * 4th Spouse: 30% of membership value + 300 EGP × years
* Partial year counts as full year. * Years = from marriage date or membership acquisition, whichever LATER
* Late addition (after membership creation): +570 EGP form fee + annual subscription * Partial year = full year (كسر السنة سنة كاملة)
* ══════════════════════════════════════════════════════════════════ * Late addition = +570 EGP form fee
* ═══════════════════════════════════════════════════════════════
*/ */
final class SpouseFeeCalculator final class SpouseFeeCalculator
{ {
/**
* Calculate the complete fee for adding a spouse.
*/
public static function calculate(int $memberId, array $spouseData): array public static function calculate(int $memberId, array $spouseData): array
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [$memberId]); $member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) { if (!$member) {
return ['error' => 'العضو غير موجود', 'fee' => '0.00', 'total' => '0.00']; return self::error('العضو غير موجود');
} }
$membershipValue = $member['membership_value'] ?? '0.00'; $membershipValue = $member['membership_value'] ?? '0.00';
if (bccomp($membershipValue, '0.00', 2) <= 0) {
return ['error' => 'قيمة العضوية غير محددة', 'fee' => '0.00', 'total' => '0.00']; // ── HARD BLOCK: No membership value = no spouse ──
if (bccomp($membershipValue, '0.01', 2) < 0) {
return self::error('يجب تحديد قيمة العضوية أولاً (ملء الاستمارة واختيار المؤهل)');
} }
// Determine spouse order // ── Determine spouse order ──
$existingCount = Spouse::countActiveForMember($memberId); $existingCount = Spouse::countActiveForMember($memberId);
$spouseOrder = $existingCount + 1; $spouseOrder = $existingCount + 1;
// Is this a late addition? (membership already active/created) // ── Is this during initial creation or later? ──
$isLateAddition = !in_array($member['status'], ['potential']); // "initial" = member status is still potential AND no membership_fee/down_payment paid yet
$isInitialCreation = in_array($member['status'] ?? '', ['potential', 'under_review']);
// Even during initial creation, only the FIRST spouse is free
// 2nd/3rd/4th ALWAYS have fees
$isFirstSpouseDuringCreation = ($spouseOrder === 1 && $isInitialCreation);
// Late addition = member is already active/accepted = must pay form fee
$isLateAddition = !in_array($member['status'] ?? '', ['potential', 'under_review']);
$formFee = $isLateAddition ? '570.00' : '0.00'; $formFee = $isLateAddition ? '570.00' : '0.00';
// Is the member "acquired" (مكتسب) or "basis" (أساس)? // ── Is the member acquired (مكتسب)? ──
// Acquired = got membership through transfer/separation/divorce/waiver
$isAcquiredMember = self::isAcquiredMember($memberId); $isAcquiredMember = self::isAcquiredMember($memberId);
// Spouse nationality // ── Nationality check ──
$nationality = trim($spouseData['nationality'] ?? 'مصري'); $nationality = trim($spouseData['nationality'] ?? 'مصري');
$isForeign = ($nationality !== 'مصري' && $nationality !== '' && $nationality !== 'Egyptian'); $isForeign = ($nationality !== 'مصري' && $nationality !== '' && $nationality !== 'Egyptian');
// Marriage date (for per-year calculation) // ── Marriage date for per-year calculation ──
$marriageDate = $spouseData['marriage_date'] ?? null; $marriageDate = $spouseData['marriage_date'] ?? null;
// Membership acquisition date
$memberCreatedDate = substr($member['created_at'] ?? date('Y-m-d'), 0, 10); $memberCreatedDate = substr($member['created_at'] ?? date('Y-m-d'), 0, 10);
// Calculate fees based on spouse order // ── Calculate based on spouse order ──
$percentageFee = '0.00';
$percentage = '0.00'; $percentage = '0.00';
$percentageFee = '0.00';
$annualPerYear = '0.00'; $annualPerYear = '0.00';
$yearCount = 0; $yearCount = 0;
$yearlyTotal = '0.00'; $yearlyTotal = '0.00';
$ruleApplied = ''; $ruleApplied = '';
if ($spouseOrder === 1) { switch (true) {
// ── 1st Spouse ── // ═══ 1ST SPOUSE ═══
if (!$isLateAddition) { case ($spouseOrder === 1):
// During creation: FREE (included in base) if ($isFirstSpouseDuringCreation && !$isForeign) {
$percentage = '0.00'; // Included in base price — truly free
$ruleApplied = 'الزوجة الأولى — مشمولة في القيمة الأساسية'; $percentage = '0.00';
} elseif ($isForeign) { $ruleApplied = 'الزوجة الأولى — مشمولة في قيمة العضوية الأساسية (بدون رسوم إضافية)';
// Foreign spouse: 15% } elseif ($isForeign) {
$percentage = '15.00'; $percentage = '15.00';
$ruleApplied = 'زوج/ة أجنبي — 15%'; $ruleApplied = 'زوج/ة أجنبي — 15% من قيمة العضوية';
} elseif ($isAcquiredMember) { } elseif ($isAcquiredMember) {
// Acquired member adding 1st spouse: 50% $percentage = '50.00';
$percentage = '50.00'; $ruleApplied = 'إضافة زوج/ة لعضو مكتسب العضوية (فصل/طلاق/وفاة/تنازل) — 50% من قيمة العضوية';
$ruleApplied = 'إضافة زوج/ة لعضو مكتسب العضوية — 50%'; } else {
} else { $percentage = '15.00';
// Basis member adding 1st spouse late: 15% $ruleApplied = 'إضافة زوج/ة لعضو أساس العضوية (إضافة لاحقة) — 15% من قيمة العضوية';
$percentage = '15.00'; }
$ruleApplied = 'إضافة زوج/ة لعضو أساس العضوية — 15%'; $percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2);
} break;
$percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2); // ═══ 2ND SPOUSE ═══
case ($spouseOrder === 2):
} elseif ($spouseOrder === 2) { $percentage = '10.00';
// ── 2nd Spouse ── $annualPerYear = '150.00';
$percentage = '10.00'; $ruleApplied = 'الزوجة الثانية — 10% من قيمة العضوية + 150 ج.م عن كل سنة';
$annualPerYear = '150.00'; $percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2);
$ruleApplied = 'الزوجة الثانية — 10% + 150 ج.م/سنة'; $yearCount = self::calculateYears($marriageDate, $memberCreatedDate);
$yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2);
$percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2); break;
$yearCount = self::calculateYears($marriageDate, $memberCreatedDate);
$yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2); // ═══ 3RD SPOUSE ═══
case ($spouseOrder === 3):
} elseif ($spouseOrder === 3) { $percentage = '20.00';
// ── 3rd Spouse ── $annualPerYear = '200.00';
$percentage = '20.00'; $ruleApplied = 'الزوجة الثالثة — 20% من قيمة العضوية + 200 ج.م عن كل سنة';
$annualPerYear = '200.00'; $percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2);
$ruleApplied = 'الزوجة الثالثة — 20% + 200 ج.م/سنة'; $yearCount = self::calculateYears($marriageDate, $memberCreatedDate);
$yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2);
$percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2); break;
$yearCount = self::calculateYears($marriageDate, $memberCreatedDate);
$yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2); // ═══ 4TH SPOUSE ═══
case ($spouseOrder >= 4):
} elseif ($spouseOrder >= 4) { $percentage = '30.00';
// ── 4th Spouse ── $annualPerYear = '300.00';
$percentage = '30.00'; $ruleApplied = 'الزوجة الرابعة — 30% من قيمة العضوية + 300 ج.م عن كل سنة';
$annualPerYear = '300.00'; $percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2);
$ruleApplied = 'الزوجة الرابعة — 30% + 300 ج.م/سنة'; $yearCount = self::calculateYears($marriageDate, $memberCreatedDate);
$yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2);
$percentageFee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2); break;
$yearCount = self::calculateYears($marriageDate, $memberCreatedDate);
$yearlyTotal = bcmul($annualPerYear, (string) $yearCount, 2);
}
// Override for foreign spouse regardless of order
if ($isForeign && $spouseOrder === 1) {
// Already handled above
} elseif ($isForeign && $spouseOrder > 1) {
// Foreign spouse who is 2nd/3rd/4th still pays normal 2nd/3rd/4th rates
// The 15% foreign rule is for 1st spouse only
} }
// Total addition fee (percentage + yearly) // ── Total ──
$additionFee = bcadd($percentageFee, $yearlyTotal, 2); $additionFee = bcadd($percentageFee, $yearlyTotal, 2);
// Grand total (addition fee + form fee)
$totalFee = bcadd($additionFee, $formFee, 2); $totalFee = bcadd($additionFee, $formFee, 2);
// ── SANITY CHECK: 2nd+ spouse must NEVER be free ──
if ($spouseOrder >= 2 && bccomp($additionFee, '0.01', 2) < 0) {
// This should never happen if membership_value > 0
return self::error(
'خطأ في حساب الرسوم — الزوجة رقم ' . $spouseOrder .
' يجب أن تكون برسوم. قيمة العضوية: ' . money($membershipValue)
);
}
return [ return [
'spouse_order' => $spouseOrder, 'spouse_order' => $spouseOrder,
'membership_value' => $membershipValue, 'membership_value' => $membershipValue,
'is_initial' => $isInitialCreation,
'is_late_addition' => $isLateAddition, 'is_late_addition' => $isLateAddition,
'is_acquired' => $isAcquiredMember, 'is_acquired' => $isAcquiredMember,
'is_foreign' => $isForeign, 'is_foreign' => $isForeign,
...@@ -156,116 +158,134 @@ final class SpouseFeeCalculator ...@@ -156,116 +158,134 @@ final class SpouseFeeCalculator
'rule_applied' => $ruleApplied, 'rule_applied' => $ruleApplied,
'error' => null, 'error' => null,
'breakdown' => self::buildBreakdown( 'breakdown' => self::buildBreakdown(
$spouseOrder, $percentage, $percentageFee, $spouseOrder, $membershipValue, $percentage, $percentageFee,
$annualPerYear, $yearCount, $yearlyTotal, $annualPerYear, $yearCount, $yearlyTotal,
$formFee, $totalFee, $ruleApplied $formFee, $totalFee, $ruleApplied, $isFirstSpouseDuringCreation
), ),
]; ];
} }
/** /**
* Calculate number of years from marriage date or acquisition date (whichever is later). * Years from marriage date or membership acquisition (whichever later).
* Partial year counts as full year (كسر السنة سنة كاملة). * Partial year = full year.
*/ */
private static function calculateYears(?string $marriageDate, string $memberCreatedDate): int private static function calculateYears(?string $marriageDate, string $memberCreatedDate): int
{ {
if (!$marriageDate) { if (!$marriageDate) {
return 1; // Default to 1 year if no marriage date return 1;
} }
// Use whichever is LATER: marriage date or membership acquisition date
$marriageTs = strtotime($marriageDate); $marriageTs = strtotime($marriageDate);
$memberTs = strtotime($memberCreatedDate); $memberTs = strtotime($memberCreatedDate);
$startTs = max($marriageTs, $memberTs);
$startDate = date('Y-m-d', $startTs); if (!$marriageTs || !$memberTs) {
return 1;
}
// Use whichever is LATER
$startTs = max($marriageTs, $memberTs);
$start = new \DateTime(date('Y-m-d', $startTs));
$now = new \DateTime(); $now = new \DateTime();
$start = new \DateTime($startDate);
if ($now <= $start) { if ($now <= $start) {
return 1; // Minimum 1 year return 1;
} }
$diff = $now->diff($start); $diff = $now->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); // Minimum 1 year return max(1, $years);
} }
/** /**
* Check if a member is "acquired" (مكتسب العضوية) — got membership through transfer/separation. * Check if member got membership through transfer/separation/divorce/death/waiver.
*/ */
private static function isAcquiredMember(int $memberId): bool private static function isAcquiredMember(int $memberId): bool
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
// Check if this member was created through a transfer, divorce, death, or waiver $tables = [
try { ['transfer_requests', 'target_member_id'],
// Check transfer_requests where this member is the target ['divorce_cases', 'spouse_new_member_id'],
$transfer = $db->selectOne( ['death_cases', 'transferred_to_member_id'],
"SELECT id FROM transfer_requests WHERE target_member_id = ? AND status = 'completed' LIMIT 1", ['waiver_requests', 'target_member_id'],
[$memberId] ];
);
if ($transfer) return true;
// Check divorce cases
$divorce = $db->selectOne(
"SELECT id FROM divorce_cases WHERE spouse_new_member_id = ? AND status = 'completed' LIMIT 1",
[$memberId]
);
if ($divorce) return true;
// Check death transfers
$death = $db->selectOne(
"SELECT id FROM death_cases WHERE transferred_to_member_id = ? AND status = 'completed' LIMIT 1",
[$memberId]
);
if ($death) return true;
// Check waivers foreach ($tables as [$table, $column]) {
$waiver = $db->selectOne( try {
"SELECT id FROM waiver_requests WHERE target_member_id = ? AND status = 'completed' LIMIT 1", if (!$db->tableExists($table)) continue;
[$memberId] $row = $db->selectOne(
); "SELECT id FROM `{$table}` WHERE `{$column}` = ? AND status = 'completed' LIMIT 1",
if ($waiver) return true; [$memberId]
} catch (\Throwable $e) { );
// Tables might not exist yet if ($row) return true;
} catch (\Throwable $e) {
continue;
}
} }
return false; return false;
} }
/** /**
* Build human-readable breakdown of fee calculation. * Build detailed Arabic breakdown.
*/ */
private static function buildBreakdown( private static function buildBreakdown(
int $order, string $pct, string $pctFee, int $order, string $membershipValue, string $pct, string $pctFee,
string $annual, int $years, string $yearlyTotal, string $annual, int $years, string $yearlyTotal,
string $formFee, string $totalFee, string $rule string $formFee, string $totalFee, string $rule, bool $isFirstFree
): array { ): array {
$lines = []; $lines = [];
$lines[] = "القاعدة: {$rule}"; $lines[] = '📋 القاعدة المطبقة: ' . $rule;
$lines[] = '💰 قيمة العضوية: ' . money($membershipValue);
if (bccomp($pctFee, '0', 2) > 0) {
$lines[] = "نسبة {$pct}% من قيمة العضوية = " . money($pctFee); if ($isFirstFree) {
} $lines[] = '✅ الزوجة الأولى مشمولة في قيمة العضوية — بدون رسوم إضافية';
} else {
if (bccomp($pctFee, '0', 2) > 0) {
$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);
}
} }
$lines[] = "الإجمالي: " . money($totalFee); $lines[] = '═══════════════════════════';
$lines[] = '💵 الإجمالي المطلوب: ' . money($totalFee);
return $lines; return $lines;
} }
/**
* Return error result.
*/
private static function error(string $message): array
{
return [
'spouse_order' => 0,
'membership_value' => '0.00',
'percentage' => '0.00',
'percentage_fee' => '0.00',
'annual_per_year' => '0.00',
'year_count' => 0,
'yearly_total' => '0.00',
'addition_fee' => '0.00',
'form_fee' => '0.00',
'total_fee' => '0.00',
'rule_applied' => '',
'error' => $message,
'breakdown' => ['❌ ' . $message],
];
}
} }
\ No newline at end of file
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