Commit 02b693a7 authored by Fares's avatar Fares

fix(sports/foreign/seasonal/honorary): registration CTA shows only once, sports sends to treasury

Three fixes applied across all non-working membership types:

1. Sports registration now calculates 50% fee and sends payment request
   to treasury automatically (like Seasonal already did). Also fixed
   billing fallback to find branch pricing without qualification_id.

2. Type-specific CTA now checks if registration record already exists
   in the DB (foreign_member_details, sports_members, etc.) — won't
   reappear after filling the form regardless of billing state.

3. Added duplicate-registration guards to all four type controllers
   (foreign, sports, seasonal, honorary) so users can't accidentally
   submit the registration form twice.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 8d2002a1
...@@ -28,6 +28,9 @@ class ForeignController extends Controller ...@@ -28,6 +28,9 @@ class ForeignController extends Controller
return $this->redirect("/members/{$memberId}")->withError('هذا العضو ليس عضواً أجنبياً'); return $this->redirect("/members/{$memberId}")->withError('هذا العضو ليس عضواً أجنبياً');
} }
$existing = $db->selectOne("SELECT id FROM foreign_member_details WHERE member_id = ? AND is_archived = 0 LIMIT 1", [(int) $memberId]);
if ($existing) return $this->redirect("/members/{$memberId}")->withError('بيانات العضوية الأجنبية مسجلة بالفعل');
$countries = $db->select("SELECT id, name_ar, nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar"); $countries = $db->select("SELECT id, name_ar, nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar");
$branchCode = 'sheraton'; $branchCode = 'sheraton';
......
...@@ -28,6 +28,9 @@ class HonoraryController extends Controller ...@@ -28,6 +28,9 @@ class HonoraryController extends Controller
return $this->redirect("/members/{$memberId}")->withError('هذا العضو ليس عضواً شرفياً'); return $this->redirect("/members/{$memberId}")->withError('هذا العضو ليس عضواً شرفياً');
} }
$existing = $db->selectOne("SELECT id FROM honorary_members WHERE member_id = ? AND is_archived = 0 LIMIT 1", [(int) $memberId]);
if ($existing) return $this->redirect("/members/{$memberId}")->withError('العضوية الشرفية مسجلة بالفعل');
$durationData = RuleEngine::get('HONORARY_DURATION_YEARS'); $durationData = RuleEngine::get('HONORARY_DURATION_YEARS');
$years = $durationData['years'] ?? 1; $years = $durationData['years'] ?? 1;
......
...@@ -256,11 +256,18 @@ final class BillingService ...@@ -256,11 +256,18 @@ final class BillingService
$items[] = $formFeeData['item']; $items[] = $formFeeData['item'];
$membershipValue = $member['membership_value'] ?? '0.00'; $membershipValue = $member['membership_value'] ?? '0.00';
if (bccomp($membershipValue, '0.01', 2) < 0 && !empty($member['qualification_id']) && !empty($member['branch_id'])) { if (bccomp($membershipValue, '0.01', 2) < 0 && !empty($member['branch_id'])) {
$pricing = $db->selectOne( if (!empty($member['qualification_id'])) {
"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", $pricing = $db->selectOne(
[(int) $member['branch_id'], (int) $member['qualification_id']] "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']]
);
} else {
$pricing = $db->selectOne(
"SELECT price FROM pricing_configs WHERE branch_id = ? AND membership_type = 'working' AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY price DESC LIMIT 1",
[(int) $member['branch_id']]
);
}
if ($pricing && bccomp($pricing['price'], '0.01', 2) >= 0) { if ($pricing && bccomp($pricing['price'], '0.01', 2) >= 0) {
$membershipValue = $pricing['price']; $membershipValue = $pricing['price'];
} }
......
...@@ -287,7 +287,16 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false)); ...@@ -287,7 +287,16 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false));
<?php endif; ?> <?php endif; ?>
<!-- Type-specific registration CTA (seasonal/foreign/sports) --> <!-- Type-specific registration CTA (seasonal/foreign/sports) -->
<?php if ($member->status === 'potential' && ($member->membership_type ?? 'working') !== 'working' && $bill['form_fee_paid'] && bccomp($bill['total_pending'] ?? '0', '0.01', 2) < 0 && empty($pendingMembership) && !($bill['membership_pending'] ?? false) && !($bill['membership_paid'] ?? false)): ?> <?php
$typeDetailExists = match ($member->membership_type ?? 'working') {
'foreign' => (bool) \App\Core\App::getInstance()->db()->selectOne("SELECT 1 FROM foreign_member_details WHERE member_id = ? AND is_archived = 0 LIMIT 1", [(int) $member->id]),
'sports' => (bool) \App\Core\App::getInstance()->db()->selectOne("SELECT 1 FROM sports_members WHERE member_id = ? AND is_archived = 0 LIMIT 1", [(int) $member->id]),
'seasonal' => (bool) \App\Core\App::getInstance()->db()->selectOne("SELECT 1 FROM seasonal_memberships WHERE member_id = ? AND person_type = 'member' AND is_archived = 0 LIMIT 1", [(int) $member->id]),
'honorary' => (bool) \App\Core\App::getInstance()->db()->selectOne("SELECT 1 FROM honorary_members WHERE member_id = ? AND is_archived = 0 LIMIT 1", [(int) $member->id]),
default => false,
};
?>
<?php if ($member->status === 'potential' && ($member->membership_type ?? 'working') !== 'working' && $bill['form_fee_paid'] && !$typeDetailExists && empty($pendingMembership) && !($bill['membership_pending'] ?? false) && !($bill['membership_paid'] ?? false)): ?>
<?php <?php
$typeFormRoutes = [ $typeFormRoutes = [
'seasonal' => ['route' => '/members/' . (int) $member->id . '/seasonal/create', 'label' => 'تسجيل بيانات العضوية الموسمية', 'icon' => '📋', 'desc' => 'سجّل بيانات المدة والأسرة — سيتم حساب الرسوم وإرسالها للخزينة تلقائياً'], 'seasonal' => ['route' => '/members/' . (int) $member->id . '/seasonal/create', 'label' => 'تسجيل بيانات العضوية الموسمية', 'icon' => '📋', 'desc' => 'سجّل بيانات المدة والأسرة — سيتم حساب الرسوم وإرسالها للخزينة تلقائياً'],
......
...@@ -50,6 +50,9 @@ class SeasonalController extends Controller ...@@ -50,6 +50,9 @@ class SeasonalController extends Controller
return $this->redirect("/members/{$memberId}")->withError('هذا العضو ليس عضواً موسمياً'); return $this->redirect("/members/{$memberId}")->withError('هذا العضو ليس عضواً موسمياً');
} }
$existing = $db->selectOne("SELECT id FROM seasonal_memberships WHERE member_id = ? AND person_type = 'member' AND is_archived = 0 LIMIT 1", [(int) $memberId]);
if ($existing) return $this->redirect("/members/{$memberId}")->withError('بيانات العضوية الموسمية مسجلة بالفعل');
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar"); $branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
$spouses = $db->select("SELECT id, full_name_ar FROM spouses WHERE member_id = ? AND is_archived = 0 AND status = 'active'", [(int) $memberId]); $spouses = $db->select("SELECT id, full_name_ar FROM spouses WHERE member_id = ? AND is_archived = 0 AND status = 'active'", [(int) $memberId]);
......
...@@ -10,6 +10,8 @@ use App\Core\App; ...@@ -10,6 +10,8 @@ use App\Core\App;
use App\Core\EventBus; use App\Core\EventBus;
use App\Modules\Sports\Models\SportsMember; use App\Modules\Sports\Models\SportsMember;
use App\Modules\Sports\Services\SportsConversionCalculator; use App\Modules\Sports\Services\SportsConversionCalculator;
use App\Modules\Members\Services\MembershipRulesService;
use App\Modules\Cashier\Services\PaymentRequestService;
class SportsController extends Controller class SportsController extends Controller
{ {
...@@ -80,6 +82,52 @@ class SportsController extends Controller ...@@ -80,6 +82,52 @@ class SportsController extends Controller
EventBus::dispatch('sports.registered', ['member_id' => (int) $memberId, 'sports_id' => (int) $sports->id]); EventBus::dispatch('sports.registered', ['member_id' => (int) $memberId, 'sports_id' => (int) $sports->id]);
$membershipValue = $member['membership_value'] ?? '0.00';
if (bccomp($membershipValue, '0.01', 2) < 0 && !empty($member['branch_id'])) {
if (!empty($member['qualification_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']]
);
} else {
$pricing = $db->selectOne(
"SELECT price FROM pricing_configs WHERE branch_id = ? AND membership_type = 'working' AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY price DESC LIMIT 1",
[(int) $member['branch_id']]
);
}
if ($pricing && bccomp($pricing['price'], '0.01', 2) >= 0) {
$membershipValue = $pricing['price'];
}
}
$conversionRules = MembershipRulesService::getAthleticMemberConversionRules();
$pct = $conversionRules['conversion_percentage'] ?? '50';
$sportsFee = bcdiv(bcmul($membershipValue, $pct, 4), '100', 2);
if (bccomp($sportsFee, '0.01', 2) >= 0) {
$breakdown = [
'🏅 رسوم عضوية رياضية',
'قيمة العضوية الأساسية: ' . number_format((float) $membershipValue, 2) . ' ج.م',
'نسبة التحويل: ' . $pct . '%',
'═══════════════════════════',
'💵 رسوم العضوية الرياضية: ' . number_format((float) $sportsFee, 2) . ' ج.م',
];
PaymentRequestService::createRequest([
'member_id' => (int) $memberId,
'amount' => $sportsFee,
'payment_type' => 'sports_membership_fee',
'related_entity_type' => 'members',
'related_entity_id' => (int) $memberId,
'description_ar' => 'رسوم عضوية رياضية — استمارة ' . ($member['form_number'] ?? ''),
'notes' => json_encode(['fee_breakdown' => $breakdown], JSON_UNESCAPED_UNICODE),
]);
return $this->redirect("/members/{$memberId}")->withSuccess(
'تم تسجيل العضوية الرياضية — الرسوم: ' . money($sportsFee) . ' — تم إرسالها للخزينة'
);
}
return $this->redirect("/members/{$memberId}")->withSuccess('تم تسجيل العضوية الرياضية' . ($yearsOfService >= $minYears ? ' — مؤهل للتحويل لعضو عامل' : '')); return $this->redirect("/members/{$memberId}")->withSuccess('تم تسجيل العضوية الرياضية' . ($yearsOfService >= $minYears ? ' — مؤهل للتحويل لعضو عامل' : ''));
} }
......
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