Commit 9548bd3b authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(pricing): Sheraton 2026 offers + above_medium qualification + fix show.php crash

- Add board offer "عروض عضويات شيراتون 2026" with 4 tiers (cash 10%, 24mo 0%, 40mo 15%, 60mo 15%)
- Add "فوق المتوسط" qualification at 187,500 EGP
- Fix crash in show.php: board_offers uses title_ar not name_ar
- Rewrite discounts tutorial with full regulatory discount coverage
Co-Authored-By: 's avatarClaude Opus 4.6 (1M context) <noreply@anthropic.com>
parent 40f1c0c0
...@@ -485,7 +485,7 @@ $_memberInitials = mb_substr($member->full_name_ar, 0, 2); ...@@ -485,7 +485,7 @@ $_memberInitials = mb_substr($member->full_name_ar, 0, 2);
<div style="font-size:12px;color:#374151;"> <div style="font-size:12px;color:#374151;">
<strong>العروض المتاحة:</strong> <strong>العروض المتاحة:</strong>
<?php foreach ($boardOffers as $bo): ?> <?php foreach ($boardOffers as $bo): ?>
<span style="background:#EDE9FE;padding:2px 8px;border-radius:4px;margin:0 2px;"><?= e($bo['name_ar']) ?></span> <span style="background:#EDE9FE;padding:2px 8px;border-radius:4px;margin:0 2px;"><?= e($bo['title_ar']) ?></span>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php else: ?> <?php else: ?>
......
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
// ─── 1. Add "above_medium" qualification if missing ───
$aboveMedium = $db->selectOne("SELECT id FROM qualifications WHERE code = 'above_medium'");
if (!$aboveMedium) {
$db->insert('qualifications', [
'code' => 'above_medium',
'name_ar' => 'مؤهل فوق المتوسط',
'name_en' => 'Above Medium Qualification',
'sort_order' => 2,
'is_active' => 1,
]);
// Push medium and none down
$db->query("UPDATE qualifications SET sort_order = 3 WHERE code = 'medium'", []);
$db->query("UPDATE qualifications SET sort_order = 4 WHERE code = 'none'", []);
}
// ─── 2. Add pricing config for above_medium (187,500) ───
$aboveMediumId = (int) ($db->selectOne("SELECT id FROM qualifications WHERE code = 'above_medium'")['id'] ?? 0);
if ($aboveMediumId > 0) {
$branches = $db->select("SELECT id, branch_code FROM branches WHERE is_active = 1");
foreach ($branches as $branch) {
$existing = $db->selectOne(
"SELECT id FROM pricing_configs WHERE branch_id = ? AND qualification_id = ? AND effective_from = '2024-07-01' AND membership_type = 'working'",
[(int) $branch['id'], $aboveMediumId]
);
if (!$existing) {
$db->insert('pricing_configs', [
'branch_id' => (int) $branch['id'],
'qualification_id' => $aboveMediumId,
'membership_type' => 'working',
'price' => '187500.00',
'currency' => 'EGP',
'effective_from' => '2024-07-01',
'effective_to' => null,
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
}
}
}
// ─── 3. Create Board Offer: "عروض عضويات شيراتون 2026" ───
$existingOffer = $db->selectOne(
"SELECT id FROM board_offers WHERE title_ar = 'عروض عضويات شيراتون 2026'"
);
if ($existingOffer) {
return;
}
$offerId = $db->insert('board_offers', [
'title_ar' => 'عروض عضويات شيراتون 2026',
'title_en' => 'Sheraton Membership Offers 2026',
'description' => 'عروض الدفع للعضويات — كاش بخصم 10% أو تقسيط على 24/40/60 شهر',
'cash_discount_type' => 'percentage',
'cash_discount_value' => '10.00',
'inst_down_payment_pct' => '50.00',
'inst_months' => 24,
'inst_interest_rate' => '0.00',
'inst_grace_type' => null,
'inst_grace_months' => 0,
'inst_post_grace_rate' => null,
'branch_id' => null,
'applies_to' => 'membership_fee',
'effective_from' => '2026-01-01',
'effective_to' => '2026-12-31',
'is_active' => 1,
'board_decision_number' => null,
'board_decision_date' => null,
'notes' => 'من ملف اسعار عضويات شيراتون — 4 مستويات دفع',
'created_by' => null,
'updated_by' => null,
'created_at' => $ts,
'updated_at' => $ts,
]);
// ─── 4. Create 4 Tiers ───
$tiers = [
[
'tier_order' => 1,
'tier_name_ar' => 'كاش كامل — خصم 10%',
'payment_type' => 'cash',
'cash_discount_pct' => '10.00',
'down_payment_pct' => null,
'months' => null,
'interest_rate' => null,
],
[
'tier_order' => 2,
'tier_name_ar' => 'تقسيط 24 شهر — بدون فوائد',
'payment_type' => 'installment',
'cash_discount_pct' => null,
'down_payment_pct' => '50.00',
'months' => 24,
'interest_rate' => '0.00',
],
[
'tier_order' => 3,
'tier_name_ar' => 'تقسيط 40 شهر — فائدة 15%',
'payment_type' => 'installment',
'cash_discount_pct' => null,
'down_payment_pct' => '33.33',
'months' => 40,
'interest_rate' => '15.00',
],
[
'tier_order' => 4,
'tier_name_ar' => 'تقسيط 60 شهر — فائدة 15%',
'payment_type' => 'installment',
'cash_discount_pct' => null,
'down_payment_pct' => '13.33',
'months' => 60,
'interest_rate' => '15.00',
],
];
foreach ($tiers as $tier) {
$db->insert('board_offer_tiers', array_merge($tier, [
'board_offer_id' => $offerId,
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]));
}
};
...@@ -4,143 +4,448 @@ ...@@ -4,143 +4,448 @@
النظام فيه 3 طبقات منفصلة للخصومات: النظام فيه 3 طبقات منفصلة للخصومات:
| الطبقة | الغرض | من يستفيد | تطبيق | | الطبقة | الغرض | من يستفيد | التطبيق |
|--------|--------|-----------|-------| |--------|--------|-----------|---------|
| عروض مجلس الإدارة | شروط دفع (خصم كاش + أقساط مخصصة) | كل الأعضاء خلال الفترة | تلقائي | | عروض مجلس الإدارة (Board Offers) | شروط دفع مخصصة (خصم كاش + أقساط مخفضة) | كل الأعضاء خلال فترة العرض | تلقائي |
| الخصومات الخاصة | خصم على قيمة العضوية | عضو محدد أو بشرط | يدوي أو تلقائي بشرط | | الخصومات الخاصة (Special Discounts) | خصم على قيمة العضوية | عضو محدد أو بشرط سداد | يدوي أو تلقائي |
| الخصومات اللائحية (Regulatory Discounts) | خصم منصوص عليه في اللائحة | فئات محددة (موظفين، مجموعات، etc.) | يدوي + اعتماد |
| تعديل الاشتراك السنوي | نسبة على الاشتراك | كل المشتركين في سنة معينة | تلقائي عند التوليد | | تعديل الاشتراك السنوي | نسبة على الاشتراك | كل المشتركين في سنة معينة | تلقائي عند التوليد |
### هل تتراكم؟
| | Board Offer | Special Discount | Regulatory Discount |
|---|---|---|---|
| **Board Offer** | — | نعم | نعم |
| **Special Discount** | نعم | — | لا (أحدهما فقط) |
| **Regulatory Discount** | نعم | لا (أحدهما فقط) | — |
- عرض مجلس الإدارة يعمل على **المبلغ المدفوع** (بعد كل الخصومات)
- الخصم الخاص واللائحي يعملان على **قيمة العضوية الأصلية** — ويأخذ الأعلى فقط
--- ---
## 1. عروض مجلس الإدارة (Board Offers) ## 1. عروض مجلس الإدارة (Board Offers)
### الفكرة ### ما هو
قرار مجلس إدارة بشروط دفع تفضيلية لفترة محدودة. يؤثر على طريقة السداد (خصم كاش / شروط أقساط مخفضة) — لكن لا يغيّر قيمة العضوية نفسها.
### أين في النظام
**إدارة**: `/pricing/board-offers`
**صلاحية المشاهدة**: `pricing.board_offers.view`
**صلاحية الإنشاء/التعديل**: `pricing.board_offers.create` / `pricing.board_offers.edit`
---
### خطوات إنشاء عرض جديد
#### الخطوة 1: الذهاب للصفحة
انتقل إلى: **التسعير ← عروض مجلس الإدارة ← إضافة عرض جديد**
URL: `/pricing/board-offers/create`
#### الخطوة 2: البيانات الأساسية
| الحقل | الوصف | مطلوب |
|-------|-------|-------|
| عنوان العرض (عربي) | مثل "عرض الصيف 2026" | نعم |
| عنوان (إنجليزي) | ترجمة اختيارية | لا |
| الوصف | تفاصيل إضافية | لا |
| الفرع | فرع محدد أو "كل الفروع" | لا |
| تاريخ البداية | أول يوم يسري فيه العرض | نعم |
| تاريخ الانتهاء | آخر يوم يسري فيه العرض | نعم |
| رقم قرار مجلس الإدارة | للتوثيق | لا |
| تاريخ القرار | للتوثيق | لا |
| مجال التطبيق | `membership_fee` أو `all` | نعم |
#### الخطوة 3: تفعيل مسار الكاش (اختياري)
فعّل checkbox "مسار كاش" ثم حدد:
| الحقل | الخيارات | مثال |
|-------|----------|------|
| نوع الخصم | `percentage` أو `fixed_amount` | percentage |
| قيمة الخصم | النسبة أو المبلغ | 10 (= 10%) |
**النتيجة**: عضو يدفع كاش يحصل على خصم. مثال: عضوية 150,000 × 10% = يدفع 135,000
#### الخطوة 4: تفعيل مسار التقسيط (اختياري)
فعّل checkbox "مسار تقسيط" ثم حدد:
قرار مجلس إدارة بشروط دفع خاصة لفترة محدودة. مثلاً: "عرض الصيف — خصم 10% كاش أو تقسيط على 36 شهر بفائدة 18%". | الحقل | الافتراضي | مثال |
|-------|-----------|------|
| نسبة المقدم | 25% | 20% |
| عدد الأشهر | 30 | 48 |
| نسبة الفائدة السنوية | 22% | 15% |
| فترة السماح (نوع) | بدون | `first_n_months` أو `full_free_under_n` |
| أشهر السماح | 0 | 6 |
| فائدة ما بعد السماح | — | 25% |
### المسار: التسعير ← عروض مجلس الإدارة **أنواع فترة السماح**:
- `first_n_months`: أول N شهر بدون فوائد، بعدها الفائدة العادية
- `full_free_under_n`: لو إجمالي الأقساط ≤ N شهر → إعفاء كامل من الفوائد
`/pricing/board-offers` #### الخطوة 5: حفظ
### ماذا يحتوي العرض العرض يصبح نشطاً فوراً (لو تاريخ البداية ≤ اليوم) ويظهر تلقائياً في صفحة كل عضو جديد.
1. **مسار الكاش** (اختياري) — خصم يُطبَّق لو العضو دفع كامل نقداً: ---
- نوع: نسبة % أو مبلغ ثابت
- مثال: خصم 10% = عضوية 150,000 تصبح 135,000
2. **مسار التقسيط** (اختياري) — يتجاوز الإعدادات الافتراضية: ### كيف يظهر للعضو (صفحة `/members/{id}`)
- نسبة المقدم (الافتراضي 25%)
- عدد الأشهر (الافتراضي 30)
- نسبة الفائدة (الافتراضي 22%)
- فترة سماح:
- `first_n_months`: أول N شهر بدون فوائد (بعدها فائدة عادية أو مخصصة)
- `full_free_under_n`: إعفاء كامل من الفوائد لو عدد الأقساط ≤ N
3. **بيانات القرار**: رقم القرار، تاريخه، الفرع (أو كل الفروع) 1. بانر أصفر يظهر: "عرض مجلس الإدارة: {عنوان العرض}"
2. **لو فيه Tiers** (عرض متعدد المستويات):
- شبكة بطاقات — كل بطاقة = مستوى (كاش أو تقسيط)
- كل بطاقة فيها زر "إرسال للخزينة"
3. **لو فيه مسار واحد** (عرض بسيط):
- الكاش: يظهر بالسعر بعد الخصم + "وفّرت X"
- التقسيط: يظهر بالشروط الجديدة (مقدم + فائدة + أشهر)
4. **فترة السريان**: effective_from → effective_to (مطلوبين) ### ماذا يحدث عند "إرسال للخزينة"
### كيف يُطبَّق تلقائياً 1. يُنشأ `payment_request` بـ `board_offer_id` + `offer_snapshot_json`
2. الـ snapshot يحفظ شروط العرض لحظة الإرسال
3. حتى لو العرض تغيّر أو انتهى بعدها — الشروط المحفوظة هي المُطبَّقة
1. العضو يفتح صفحته `/members/{id}` ### Multi-Tier Offers (عروض متعددة المستويات)
2. الكنترولر يستدعي `BoardOfferService::getBestOffer($branchId)`
3. لو فيه عرض ساري:
- يظهر بانر أصفر "عرض مجلس الإدارة: {title}"
- الكاش يظهر بالمبلغ بعد الخصم + "وفّرت X"
- التقسيط يظهر بالشروط الجديدة (مقدم، أشهر، فائدة، سماح)
4. عند الضغط "إرسال للخزينة":
- يُخزن `board_offer_id` + `offer_snapshot_json` في `payment_requests`
- الـ snapshot يحفظ شروط العرض لحظة الطلب — تغيير العرض بعدها لا يؤثر
### الأولوية العرض الواحد يمكن أن يحتوي على عدة **Tiers** (جدول `board_offer_tiers`):
- عرض بـ `branch_id` محدد يتقدم على عرض بدون فرع (كل الفروع) | الحقل | الوصف |
- لو فيه أكثر من عرض ساري → الأكثر تحديداً يفوز (أحدث effective_from) |-------|-------|
- عرض واحد فقط يُطبَّق في المرة الواحدة | `tier_order` | ترتيب العرض (1, 2, 3...) |
| `tier_name_ar` | اسم المستوى ("كاش كامل"، "تقسيط 12 شهر"...) |
| `payment_type` | `cash` أو `installment` |
| `cash_discount_pct` | نسبة الخصم (للكاش) |
| `down_payment_pct` | نسبة المقدم (للتقسيط) |
| `months` | عدد الأشهر (للتقسيط) |
| `interest_rate` | نسبة الفائدة (للتقسيط) |
مثال: عرض بـ 3 مستويات:
1. كاش: خصم 15%
2. تقسيط 12 شهر: مقدم 30%، بدون فائدة
3. تقسيط 36 شهر: مقدم 20%، فائدة 18%
---
### Edge Cases ### Edge Cases
- **العرض انتهى أثناء ما الطلب في الخزينة**: الـ snapshot محفوظ — الخصم يُطبَّق - **العرض انتهى أثناء ما الطلب في الخزينة**: snapshot محفوظ — الخصم يُطبَّق
- **العرض اتعطّل (is_active=0)**: لا يظهر للأعضاء الجدد، لكن الطلبات القديمة ما تتأثر - **العرض اتعطّل (is_active=0)**: لا يظهر لأعضاء جدد — الطلبات القديمة لا تتأثر
- **عرض مستقبلي (effective_from > today)**: لا يظهر — ينشط تلقائياً عند الموعد - **عرض مستقبلي**: لا يظهر — ينشط تلقائياً عند effective_from
- **عرض بدون مسار كاش ولا تقسيط**: لا يُطبَّق (الفاليديشن يمنع حفظه أصلاً) - **أكثر من عرض ساري**: الأكثر تحديداً يفوز (branch_id محدد > NULL, ثم الأحدث)
- **عضو واحد يدفع كاش والتاني تقسيط**: كل واحد يأخذ المسار اللي اختاره - **بدون مسار كاش ولا تقسيط**: لا يمكن حفظه (validation يمنع)
--- ---
## 2. الخصومات الخاصة (Special Discounts) ## 2. الخصومات الخاصة (Special Discounts)
### الفكرة ### ما هو
خصم على **قيمة العضوية الأصلية** — ينقّص المبلغ المطلوب دفعه. يمكن تعيينه يدوياً لعضو أو أن يُطبَّق تلقائياً عند استيفاء شرط.
خصومات على **قيمة العضوية نفسها** — إما يدوية (تُعيَّن لعضو) أو تلقائية (بشرط). ### أين في النظام
### المسار: التسعير ← الخصومات الخاصة **إدارة**: `/pricing/special-discounts`
**صلاحية المشاهدة**: `pricing.special_discounts.view`
**صلاحية الإنشاء/التعديل**: `pricing.special_discounts.create` / `pricing.special_discounts.edit`
`/pricing/special-discounts` ---
### أنواع الخصم ### خطوات إنشاء خصم خاص جديد
| النوع | التأثير | #### الخطوة 1: الذهاب للصفحة
|-------|---------|
| `percentage` | يخصم X% من قيمة العضوية |
| `fixed_amount` | يخصم مبلغ ثابت |
| `free_subscription` | لا يخصم من العضوية — يمنح اشتراكات مجانية فقط |
### شروط التطبيق انتقل إلى: **التسعير ← الخصومات الخاصة ← إضافة خصم جديد**
| الشرط | المعنى | URL: `/pricing/special-discounts/create`
|-------|--------|
| `none` | بدون شرط — يُطبَّق يدوياً عند تعيينه لعضو |
| `full_payment` | يُطبَّق تلقائياً لو العضو سدّد كامل قيمة العضوية |
| `min_payment` | يُطبَّق تلقائياً لو العضو سدّد مبلغ ≥ الحد الأدنى |
### كيف يُعيَّن لعضو (يدوي) #### الخطوة 2: تعبئة النموذج
1. صفحة العضو `/members/{id}` | الحقل | الخيارات | ملاحظات |
2. قسم "خصم خاص" (يظهر فقط قبل التفعيل) |-------|----------|---------|
3. اختر الخصم من القائمة → احفظ | اسم الخصم (عربي) | نص حر | مطلوب. مثال: "خصم أبناء الشهداء" |
4. يُخزن `special_discount_id` في جدول `members` | اسم (إنجليزي) | نص حر | اختياري |
5. `BillingService::getMemberBill()` يحسب الخصم ويُنقصه من الفاتورة | نوع الخصم | `percentage` / `fixed_amount` / `free_subscription` | — |
| نسبة الخصم | 0.01 – 100 | فقط لنوع percentage |
| مبلغ الخصم الثابت | رقم > 0 | فقط لنوع fixed_amount |
| مجال التطبيق | `membership_fee` / `subscription` / `all` | — |
| تاريخ البداية | YYYY-MM-DD أو فارغ (ساري دائماً) | — |
| تاريخ الانتهاء | YYYY-MM-DD أو فارغ | — |
| شرط التطبيق | `none` / `full_payment` / `min_payment` | — |
| الحد الأدنى للسداد | رقم | فقط لشرط min_payment |
| سنوات اشتراك مجانية | 0 – 5 | مكافأة إضافية |
| يتطلب مستند | نعم/لا | لو نعم: العضو لازم يرفع PDF/صورة |
| وصف | نص حر | اختياري |
#### الخطوة 3: حفظ
الخصم يصبح نشطاً فوراً ومتاحاً للتعيين.
---
### كيف يُعيَّن لعضو (التطبيق اليدوي)
يوجد **3 أماكن** يمكن تعيين خصم خاص منها:
#### الطريقة 1: من صفحة العضو (الأسرع)
1. افتح صفحة العضو: `/members/{id}`
2. في قسم "خصم خاص" (يظهر فقط لو العضو في حالة `accepted` أو `payment_pending`)
3. اختر الخصم من القائمة المنسدلة
4. لو الخصم يتطلب مستند → ارفع الملف
5. اضغط "تطبيق الخصم"
**النتيجة**: يُحفظ `special_discount_id` + `discount_amount` في جدول `members`
#### الطريقة 2: من صفحة ملء الاستمارة
1. افتح صفحة ملء الاستمارة: `/members/{id}/fill-form`
2. القسم 6: "خصم خاص (اختياري)"
3. اختر الخصم → ارفع المستند (لو مطلوب)
4. يُحفظ عند إرسال الاستمارة
#### الطريقة 3: من صفحة تعديل العضو
1. افتح صفحة التعديل: `/members/{id}/edit`
2. قسم "خصم خاص" → اختر من القائمة
3. النظام يحسب المبلغ تلقائياً ويعرضه
---
### كيف يُطبَّق تلقائياً (بشرط) ### كيف يُطبَّق تلقائياً (بشرط)
1. `SpecialDiscountService::evaluateForMember($memberId, $membershipValue, $totalPaid)` يُستدعى أثناء حساب الفاتورة خصومات بـ `condition_type != 'none'` تُطبَّق تلقائياً:
2. يفحص: هل العضو معيّن له خصم؟ → يستخدمه
3. لو لا → يبحث في الخصومات الشرطية (`condition_type != 'none'`) | الشرط | المعنى | مثال |
4. يطابق الشرط (سداد كامل / حد أدنى) → يُطبَّق أعلى خصم مطابق |-------|--------|------|
| `full_payment` | العضو سدّد كامل قيمة العضوية | "ادفع كاش واحصل على 5%" |
| `min_payment` | العضو سدّد ≥ الحد الأدنى المحدد | "سدّد 100,000 واحصل على 3%" |
**كيف يعمل**: `SpecialDiscountService::evaluateForMember()` يُستدعى أثناء حساب الفاتورة → يفحص الشروط → يُطبَّق أعلى خصم مطابق
---
### المكافأة: سنوات اشتراك مجانية ### المكافأة: سنوات اشتراك مجانية
- حقل `bonus_free_subscription_years` (0-5) - حقل `bonus_free_subscription_years` (0-5)
- بعد تفعيل العضوية (event: `member.activated`): - بعد تفعيل العضوية → `applyFreeSubscriptionBonus()` يحوّل اشتراكات pending إلى paid
- `SpecialDiscountService::getBonusFreeYears($memberId)` يحسب - الإيصال يُسجَّل كـ `FREE-BONUS`
- `applyFreeSubscriptionBonus()` يحول اشتراكات pending إلى paid (بـ receipt = 'FREE-BONUS')
مثال: خصم "كاش كامل" + bonus=1 → العضو يدفع كاش → يُفعَّل → اشتراك أول سنة مجاناً
---
### كيف يُزال الخصم
1. صفحة العضو `/members/{id}`
2. لو خصم مُطبَّق → يظهر "خصم مُطبق: {اسم}" + زر "إزالة"
3. اضغط "إزالة" → يُمسح `special_discount_id` و `discount_amount`
---
### Edge Cases
- **خصم بدون فترة (dates = null)**: ساري دائماً
- **percentage = 100%**: العضوية مجاناً (مثل أبناء شهداء)
- **عضو بخصم يدوي + خصم شرطي ساري**: اليدوي يفوز (يُفحص أولاً)
- **خصم معطّل (is_active=0) ومعيّن لعضو**: لا يُطبَّق — يُتجاهل في BillingService
- **عضو دفع جزء ثم أضيف خصم**: الخصم ينقّص المتبقي فقط
---
## 3. الخصومات اللائحية (Regulatory Discounts)
### ما هو
خصومات **منصوص عليها في لائحة النادي** بموجب مواد محددة (97-102, 110). تُمنح لفئات معينة من المتقدمين بعد إثبات الأهلية واعتماد المسؤول.
### أين في النظام
**إدارة**: `/pricing/regulatory-discounts`
**طلبات الاعتماد**: `/pricing/regulatory-discounts/applications`
**صلاحية المشاهدة**: `pricing.regulatory_discounts.view`
**صلاحية الإنشاء**: `pricing.regulatory_discounts.create`
**صلاحية التعديل**: `pricing.regulatory_discounts.edit`
**صلاحية الاعتماد**: `pricing.regulatory_discounts.approve`
---
### أنواع الخصومات اللائحية (6 أنواع)
| المادة | النوع (`eligibility_type`) | الفئة المستفيدة | طريقة الإثبات |
|--------|---------------------------|-----------------|---------------|
| 97 | `cross_branch_member` | عضو فرع آخر يريد الانضمام لهذا الفرع | التحقق من عضوية نشطة في الفرع المصدر |
| 98 | `government_employee` | موظف حكومي | مستند إثبات وظيفة حكومية |
| 98/99 | `ministry_youth_employee` | موظف وزارة الشباب والرياضة | مستند وزاري + شروط تقسيط خاصة |
| 100 | `board_of_trustees` | عضو مجلس أمناء | قرار مجلس أمناء |
| 102 | `club_employee` | موظف النادي | التحقق من HR (سنوات الخدمة) |
| 110 | `group_membership` | مجموعة ≥ 5 أعضاء | عدد الأعضاء في الطلب الجماعي |
---
### خطوات إنشاء خصم لائحي جديد
#### الخطوة 1: الذهاب للصفحة
انتقل إلى: **التسعير ← الخصومات اللائحية ← إضافة خصم جديد**
URL: `/pricing/regulatory-discounts/create`
#### الخطوة 2: البيانات الأساسية
| الحقل | الوصف | مطلوب |
|-------|-------|-------|
| رقم المادة | 97, 98, 99, 100, 102, أو 110 | نعم |
| اسم الخصم (عربي) | مثل "خصم الموظف الحكومي" | نعم |
| اسم (إنجليزي) | ترجمة | لا |
| الوصف | تفاصيل | لا |
| نوع الاستحقاق | أحد الأنواع الستة | نعم |
| نسبة الخصم | 0.01 – 100 | نعم (ما عدا group_membership) |
| الحد الأقصى للنسبة | cap على النسبة | لا |
| الفرع المصدر | لـ cross_branch_member فقط | حسب النوع |
| الفرع المستهدف | الفرع اللي يُطبَّق فيه | حسب النوع |
| الحد الأدنى لسنوات الخدمة | لـ club_employee فقط | حسب النوع |
| يسمح بالتقسيط | نعم/لا | لا |
| أقصى سنوات تقسيط | لو نعم | لا |
| فائدة التقسيط | نسبة خاصة (مختلفة عن الافتراضية) | لا |
| مجال التطبيق | `membership_fee` | — |
| تاريخ البداية/الانتهاء | فترة السريان | لا |
| رقم قرار مجلس الإدارة | توثيق | لا |
| تاريخ القرار | توثيق | لا |
| ملاحظات | نص حر | لا |
#### الخطوة 3: شرائح المجموعات (لـ `group_membership` فقط)
لو اخترت نوع "عضوية مجمعة" (مادة 110)، أضف شرائح:
| الحد الأدنى | الحد الأقصى | نسبة الخصم |
|-------------|-------------|------------|
| 5 | 9 | 10% |
| 10 | 19 | 15% |
| 20 | — | 20% |
كل شريحة: `tier_min[]`, `tier_max[]`, `tier_pct[]`
#### الخطوة 4: حفظ
---
### كيف يُطبَّق على عضو
#### الطريقة 1: أثناء ملء الاستمارة (الطريقة الأساسية)
1. افتح صفحة ملء الاستمارة: `/members/{id}/fill-form`
2. انزل للقسم 7: "خصم لائحي (المواد 97-102، 110)"
3. يظهر جدول FYI بكل الخصومات المتاحة ونِسَبها
4. اختر نوع الاستحقاق من القائمة المنسدلة:
- `عضو فرع آخر (مادة 97)`
- `موظف حكومي (مادة 98)`
- `موظف وزارة الشباب (مادة 98/99)`
- `عضو مجلس أمناء (مادة 100)`
- `موظف النادي (مادة 102)`
- `عضوية مجمعة (مادة 110)`
5. أدخل البيانات الإضافية حسب النوع:
- **عضو فرع آخر**: اختر الفرع المصدر + رقم العضو
- **موظف النادي**: اختر الموظف من HR (النظام يتحقق تلقائياً)
- **مجموعة**: أدخل عدد الأعضاء (≥ 5)
6. النظام يرسل AJAX request لـ `/pricing/regulatory-discounts/check-eligibility`
7. يظهر الخصم المستحق: "خصم X% = Y ج.م"
8. ارفع مستند الإثبات (PDF أو صورة)
9. احفظ الاستمارة
**النتيجة**: يُحفظ `regulatory_discount_id` + `regulatory_discount_amount` + `regulatory_discount_document` في جدول `members`
#### الطريقة 2: من صفحة تعديل العضو
1. افتح `/members/{id}/edit`
2. قسم "خصم لائحي" → يظهر جدول FYI + dropdown
3. اختر الخصم → النظام يحسب المبلغ
4. ارفع المستند
5. احفظ
---
### دورة الاعتماد (Approval Workflow)
1. الموظف يطبّق الخصم اللائحي على العضو
2. يُنشأ سجل في `regulatory_discount_applications`:
- `status = 'pending'`
- `discount_percentage`, `original_amount`, `discount_amount`, `final_amount`
- `verification_data` (JSON — بيانات التحقق)
3. المسؤول يذهب لـ `/pricing/regulatory-discounts/applications`
4. يراجع الطلبات المعلقة → يعتمد أو يرفض
5. **الاعتماد**: `status → 'approved'`, `approved_by`, `approved_at`
6. **الرفض**: `status → 'rejected'`, `rejection_reason`
---
### التحقق التلقائي من الأهلية
النظام يتحقق تلقائياً عند الاختيار:
| النوع | طريقة التحقق |
|-------|-------------|
| `club_employee` | `RegulatoryDiscountService::verifyClubEmployee($employeeId)` — يفحص HR: هل الموظف نشط؟ كم سنة خدمة؟ |
| `cross_branch_member` | `verifyCrossBranchMembership($memberId, $sourceBranchId)` — يفحص: هل عضو نشط في الفرع المصدر؟ |
| بقية الأنواع | يعتمد على المستند المرفق + اعتماد المسؤول |
---
### أمثلة عملية
#### مثال 1: موظف حكومي يتقدم للعضوية
1. أنشئ العضو كالمعتاد
2. عند ملء الاستمارة → القسم 7 → "موظف حكومي (مادة 98)"
3. النظام يحسب: 150,000 × 25% = خصم 37,500
4. ارفع صورة بطاقة الوظيفة
5. المتبقي المطلوب: 112,500 ج.م
#### مثال 2: موظف نادي (5 سنوات خدمة)
1. عند اختيار "موظف النادي (مادة 102)"
2. النظام يتحقق من HR → يجد الموظف بـ 5 سنوات
3. لو الخصم يتطلب min_service_years=3 → مؤهل
4. يُطبَّق الخصم تلقائياً
#### مثال 3: مجموعة 15 عضو
1. اختر "عضوية مجمعة (مادة 110)"
2. أدخل العدد: 15
3. النظام يبحث في الشرائح → 10-19 = 15%
4. خصم 15% لكل فرد في المجموعة
---
### Edge Cases ### Edge Cases
- **خصم بدون فترة (effective_from/to = null)**: ساري دائماً - **خصم لائحي + خصم خاص**: كلاهما يُحفظ في members لكن `BillingService` يطبّق الاثنين كبنود منفصلة في الفاتورة
- **خصم percentage = 100%**: العضوية مجاناً — ممكن لأبناء شهداء مثلاً - **max_discount_percentage**: لو الشريحة تعطي 20% لكن الحد الأقصى 15% → يُطبَّق 15%
- **خصم condition=full_payment + bonus=1 year**: الأكثر شيوعاً — ادفع كاش واحصل على اشتراك سنة مجاني - **موظف نادي غير نشط في HR**: `verifyClubEmployee()` يرفض — لا يمكن التطبيق
- **عضو بخصم يدوي + خصم شرطي**: اليدوي يفوز (يُفحص أولاً) - **خصم يسمح بالتقسيط بشروط خاصة**: `allows_installment=1` + `installment_max_years=2` + `installment_interest_rate=0` → تقسيط سنتين بدون فائدة (مثل وزارة الشباب)
- **خصم معطّل (is_active=0) ومعيّن لعضو**: لا يُطبَّق — يُتجاهل - **خصم لائحي معطّل**: لا يظهر في dropdown — لكن لو مُطبَّق على عضو فيُعرض كمعلومة
--- ---
## 3. تعديل الاشتراك السنوي (Subscription Year Adjustment) ## 4. تعديل الاشتراك السنوي (Subscription Year Adjustment)
### الفكرة ### ما هو
خصم/زيادة على الاشتراكات السنوية لكل المشتركين في سنة مالية معينة. خصم/إعفاء على الاشتراكات السنوية لكل المشتركين في سنة مالية معينة.
### المسار: التسعير ← لوحة التسعير ### أين في النظام
`/pricing` **المسار**: `/pricing` (لوحة التسعير)
### كيف يعمل ### كيف يعمل
- Rule Engine key: `SUBSCRIPTION_YEAR_ADJUSTMENT_{year}` (مثل `SUBSCRIPTION_YEAR_ADJUSTMENT_2026`) - Rule Engine key: `SUBSCRIPTION_YEAR_ADJUSTMENT_{year}`
- يحتوي `discount_percentage` (مثل 50 = خصم 50% على الاشتراك) - يحتوي `discount_percentage` (مثل 50 = خصم 50%)
- `SubscriptionSyncService` و `SubscriptionGenerator` يطبقان الخصم عند إنشاء صف الاشتراك - `SubscriptionGenerator` يطبّق الخصم عند إنشاء سطر الاشتراك
### متى يُستخدم ### متى يُستخدم
...@@ -150,82 +455,122 @@ ...@@ -150,82 +455,122 @@
--- ---
## 4. الفرق بين Board Offer و Special Discount ## 5. مقارنة شاملة بين أنواع الخصومات
| | Board Offer | Special Discount |
|---|---|---|
| **يخصم من** | المبلغ المدفوع (كاش) أو يغيّر شروط التقسيط | قيمة العضوية في الفاتورة |
| **يُطبَّق على** | كل من يدفع خلال الفترة | عضو معيّن أو بشرط |
| **مصدره** | قرار مجلس إدارة | إدارة النادي |
| **يتراكم مع الآخر** | نعم — الاتنين ممكن ينطبقوا على نفس العضو | نعم |
| **أين يُحفظ** | `payment_requests.board_offer_id` + snapshot | `members.special_discount_id` |
| **تأثير على الفاتورة** | لا (معلوماتي فقط حتى الدفع) | نعم (ينقّص total_pending) |
### مثال تراكم | | Board Offer | Special Discount | Regulatory Discount |
|---|---|---|---|
عضو عضويته 150,000: | **يخصم من** | المبلغ المدفوع فعلياً | قيمة العضوية في الفاتورة | قيمة العضوية في الفاتورة |
1. عليه خصم خاص 10% → الفاتورة تصبح 135,000 | **يُطبَّق على** | كل من يدفع خلال الفترة | عضو معيّن أو بشرط | فئات لائحية محددة |
2. عرض مجلس إدارة كاش 10% → يدفع 121,500 | **مصدره** | قرار مجلس إدارة | قرار إدارة | نص لائحة النادي |
3. مكافأة سنة اشتراك مجانية → الاشتراك السنوي (492 + 35) = 0 | **يتطلب اعتماد** | لا | لا | نعم (applications) |
| **يتطلب مستند** | لا | اختياري | نعم (غالباً) |
| **أين يُحفظ** | `payment_requests.board_offer_id` | `members.special_discount_id` | `members.regulatory_discount_id` |
| **تأثير على الفاتورة** | لا (معلوماتي حتى الدفع) | نعم (ينقّص total_pending) | نعم (ينقّص total_pending) |
| **فترة صلاحية** | مطلوبة (from/to) | اختيارية | اختيارية |
| **يمكن إزالته** | لا (snapshot) | نعم (زر إزالة) | يدوياً من التعديل |
--- ---
## 5. دورة حياة العرض الكاملة (Board Offer Lifecycle) ## 6. مثال تراكم كامل
عضو عضويته 150,000 ج.م:
``` ```
إنشاء العرض (/pricing/board-offers/create) قيمة العضوية الأصلية: 150,000 ج.م
- خصم لائحي (مادة 98, 25%): - 37,500 ج.م
[is_active=1, effective_from ≤ today ≤ effective_to] - خصم خاص (10%): - 15,000 ج.م
↓ يظهر تلقائياً ===================================================
صفحة العضو ← بانر أصفر + أزرار كاش/تقسيط محدّثة المطلوب سداده: 97,500 ج.م
↓ العضو يختار مسار الدفع
إرسال طلب دفع ← board_offer_id + snapshot يُحفظ → العضو يختار "كاش" مع عرض مجلس (خصم 10% كاش):
97,500 × 10% = وفّر 9,750
الخزينة تستلم الطلب ← تعالجه ← يُنشئ payment المدفوع فعلياً: 87,750 ج.م
payment.completed event ← يقرأ snapshot ← يطبّق الشروط → مكافأة: سنة اشتراك مجانية (من الخصم الخاص)
اشتراك 2026/2027 (492 + 35) = مجاناً
installment_plan يُنشأ بشروط العرض (لو تقسيط)
``` ```
--- ---
## 6. دورة حياة الخصم الخاص (Special Discount Lifecycle) ## 7. السيناريوهات الشائعة — Step by Step
``` ### سيناريو 1: "عايزين عرض كاش 10% لمدة 3 شهور"
إنشاء الخصم (/pricing/special-discounts/create)
1. `/pricing/board-offers/create`
[حالة 1: بدون شرط] 2. العنوان: "عرض الصيف — خصم 10% كاش"
الموظف يعيّنه يدوياً لعضو ← members.special_discount_id = X 3. فعّل مسار الكاش → percentage → 10
4. التواريخ: اليوم → بعد 3 شهور
BillingService.getMemberBill() يحسب الخصم ← total_pending ينقص 5. احفظ ← كل عضو يدفع كاش خلال الفترة يحصل على 10% تلقائي
[حالة 2: بشرط full_payment] ### سيناريو 2: "تقسيط على 48 شهر بفائدة 15% بدل 30 شهر بـ 22%"
العضو يسدّد كامل ← evaluateForMember() يكتشف الشرط متحقق
1. `/pricing/board-offers/create`
الخصم يُطبَّق تلقائياً في الفاتورة 2. فعّل مسار التقسيط → 48 شهر → 15%
3. ممكن تفعّل الكاش أيضاً
member.activated event ← getBonusFreeYears() ← applyFreeSubscriptionBonus()
``` ### سيناريو 3: "إعفاء فوائد لو دفع في 6 شهور أو أقل"
1. مسار تقسيط → grace_type = `full_free_under_n` → grace_months = 6
2. ≤ 6 أقساط → فائدة = 0%
3. > 6 أقساط → الفائدة العادية
### سيناريو 4: "خصم 10% + سنة مجانية لأي عضو يدفع كامل"
1. `/pricing/special-discounts/create`
2. نوع: percentage → 10%
3. شرط: full_payment
4. bonus_free_subscription_years = 1
5. ← أي عضو يسدّد كامل → 10% خصم + اشتراك سنة مجاناً
### سيناريو 5: "خصم ثابت 5000 ج.م لفئة معينة بمستند"
1. `/pricing/special-discounts/create`
2. نوع: fixed_amount → 5000
3. شرط: none (يدوي)
4. requires_document = 1
5. ← الموظف يعيّنه يدوياً + يرفع المستند
### سيناريو 6: "تطبيق خصم مادة 102 لموظف نادي"
1. أنشئ العضو → ادفع رسم الاستمارة
2. افتح ملء الاستمارة → القسم 7
3. اختر "موظف النادي (مادة 102)"
4. اختر الموظف → النظام يتحقق من HR
5. يظهر "خصم X% = Y ج.م"
6. احفظ الاستمارة
### سيناريو 7: "تسجيل مجموعة 20 عضو بخصم مادة 110"
1. `/pricing/regulatory-discounts` → تأكد أن مادة 110 موجودة بشرائحها
2. عند تسجيل كل عضو → في الاستمارة اختر "عضوية مجمعة"
3. أدخل العدد: 20
4. النظام يحسب الشريحة المناسبة → يُطبَّق الخصم
--- ---
## 7. الملفات المسؤولة ## 8. الملفات المسؤولة
| الملف | الدور | | الملف | الدور |
|-------|-------| |-------|-------|
| `Pricing/Controllers/BoardOfferController.php` | CRUD عروض مجلس الإدارة | | `Pricing/Controllers/BoardOfferController.php` | CRUD عروض مجلس الإدارة |
| `Pricing/Controllers/SpecialDiscountController.php` | CRUD الخصومات الخاصة | | `Pricing/Controllers/SpecialDiscountController.php` | CRUD الخصومات الخاصة |
| `Members/Services/BoardOfferService.php` | جلب العرض الأفضل + حساب خصم كاش + شروط تقسيط | | `Pricing/Controllers/RegulatoryDiscountController.php` | CRUD + اعتماد الخصومات اللائحية |
| `Pricing/Services/SpecialDiscountService.php` | تقييم خصم عضو + تطبيق مكافأة سنوات | | `Members/Services/BoardOfferService.php` | جلب العرض الأفضل + حساب Tiers + خصم كاش + شروط تقسيط + snapshot |
| `Members/Services/BillingService.php` (line 649) | يعرض العرض كبند في الفاتورة | | `Pricing/Services/SpecialDiscountService.php` | تقييم خصم عضو + تطبيق مكافأة سنوات مجانية |
| `Members/Controllers/MemberController.php` (line 514) | يحفظ snapshot العرض عند إرسال طلب الدفع | | `Pricing/Services/RegulatoryDiscountService.php` | تقييم أهلية + التحقق من HR/عضوية + إنشاء/اعتماد طلبات |
| `Pricing/Services/PricingEngine.php` | حساب أسعار العضوية + رسوم الأبناء + الأقساط | | `Members/Services/BillingService.php` | يحسب كل الخصومات ويعرضها كبنود في الفاتورة |
| `Pricing/bootstrap.php` (line 35) | event listener: يطبّق مكافأة الاشتراك بعد التفعيل | | `Members/Controllers/MemberController.php` | `applyDiscount()` + `fillForm()` + `payMembership()` |
| `Members/Views/show.php` | عرض الخصومات + دليل FYI + أزرار التطبيق |
| `Members/Views/fill-form.php` | نموذج تطبيق الخصم اللائحي + الخاص |
| `Members/Views/edit.php` | تعديل الخصم الخاص واللائحي |
| `Pricing/Views/board_offers/form.php` | نموذج إنشاء/تعديل عرض |
| `Pricing/Views/special_discounts/form.php` | نموذج إنشاء/تعديل خصم خاص |
| `Pricing/Views/regulatory_discounts/form.php` | نموذج إنشاء/تعديل خصم لائحي |
| `Pricing/Views/regulatory_discounts/applications.php` | صفحة اعتماد الطلبات |
--- ---
## 8. جداول قاعدة البيانات ## 9. جداول قاعدة البيانات
### `board_offers` ### `board_offers`
``` ```
...@@ -241,6 +586,15 @@ board_decision_number, board_decision_date, notes, ...@@ -241,6 +586,15 @@ board_decision_number, board_decision_date, notes,
created_by, updated_by, created_at, updated_at created_by, updated_by, created_at, updated_at
``` ```
### `board_offer_tiers`
```
id, board_offer_id (FK), tier_order, tier_name_ar,
payment_type (cash|installment),
cash_discount_pct,
down_payment_pct, months, interest_rate,
is_active, created_at, updated_at
```
### `special_discounts` ### `special_discounts`
``` ```
id, name_ar, name_en, id, name_ar, name_en,
...@@ -256,55 +610,64 @@ description, is_active, ...@@ -256,55 +610,64 @@ description, is_active,
created_at, updated_at, created_by, updated_by created_at, updated_at, created_by, updated_by
``` ```
### `payment_requests` (الحقول المتعلقة) ### `regulatory_discounts`
``` ```
board_offer_id (FK|NULL), id, article_number, name_ar, name_en, description,
offer_snapshot_json (JSON string of offer at request time) discount_percentage, max_discount_percentage,
eligibility_type (cross_branch_member|government_employee|ministry_youth_employee|board_of_trustees|club_employee|group_membership),
source_branch_id (FK|NULL), target_branch_id (FK|NULL),
min_service_years (for club_employee),
allows_installment (0|1), installment_max_years, installment_interest_rate,
applies_to, effective_from, effective_to,
board_decision_number, board_decision_date, notes,
is_active, created_by, updated_by, created_at, updated_at
``` ```
### `members` (الحقول المتعلقة) ### `regulatory_discount_group_tiers`
``` ```
special_discount_id (FK|NULL) id, regulatory_discount_id (FK),
min_quantity, max_quantity (nullable),
discount_percentage, is_active
``` ```
--- ### `regulatory_discount_applications`
```
## 9. سيناريوهات شائعة id, regulatory_discount_id (FK), member_id (FK|NULL),
applicant_name, discount_percentage,
### "عايزين عرض كاش 10% لمدة 3 شهور" original_amount, discount_amount, final_amount,
1. `/pricing/board-offers/create` status (pending|approved|rejected),
2. العنوان: "عرض الصيف — خصم 10% كاش" verification_data (JSON), notes,
3. فعّل مسار الكاش → percentage → 10 rejection_reason,
4. التواريخ: اليوم → بعد 3 شهور created_by, approved_by, approved_at,
5. احفظ ← كل عضو يدفع كاش خلال الفترة يحصل على 10% خصم تلقائي created_at, updated_at
```
### "عايزين تقسيط على 48 شهر بفائدة 15% بدل 30 شهر بـ 22%"
1. `/pricing/board-offers/create`
2. فعّل مسار التقسيط → 48 شهر → 15% فائدة
3. ممكن تفعّل الكاش أيضاً (العرض يقدم المسارين)
### "عايزين إعفاء من الفوائد لو دفع في 6 شهور أو أقل"
1. مسار تقسيط → grace_type = `full_free_under_n` → grace_months = 6
2. لو العضو اختار 6 أقساط أو أقل → فائدة = 0%
3. لو اختار 7+ → الفائدة العادية تُطبَّق
### "عايزين خصم 10% لأي عضو يدفع كامل" ### `members` (الحقول المتعلقة بالخصومات)
1. `/pricing/special-discounts/create` ```
2. نوع: percentage → 10% special_discount_id (FK|NULL),
3. شرط: full_payment discount_amount (calculated),
4. مجال: membership_fee special_discount_document (file path),
5. ← أي عضو يسدّد كامل العضوية نقداً يحصل على 10% تلقائياً regulatory_discount_id (FK|NULL),
regulatory_discount_amount (calculated),
regulatory_discount_document (file path),
regulatory_eligibility_type (enum)
```
### "عايزين سنة اشتراك مجاناً لأي عضو يدفع كاش" ### `payment_requests` (الحقول المتعلقة)
1. نفس الخصم أعلاه + bonus_free_subscription_years = 1 ```
2. بعد تفعيل العضوية → اشتراك السنة الحالية يتحول لـ "paid" تلقائياً board_offer_id (FK|NULL),
offer_snapshot_json (JSON — frozen offer state at request time)
```
--- ---
## 10. ما لا يجب فعله ## 10. ما لا يجب فعله
- **لا تنشئ board offer + special discount بنفس النسبة** — هيتراكموا والعضو يأخذ خصم مضاعف - **لا تنشئ board offer + special discount بنفس النسبة بدون قصد** — هيتراكموا
- **لا تعدّل عرض ساري والطلبات في الخزينة** — الطلبات القديمة محمية بالـ snapshot، بس ممكن يسبب لخبطة في التقارير - **لا تعدّل عرض ساري والطلبات في الخزينة** — الطلبات محمية بالـ snapshot لكن يسبب لخبطة تقارير
- **لا تحذف خصم معيّن لعضو** — عطّله (is_active=0) بدل ما تحذفه، عشان السجل - **لا تحذف خصم مُعيَّن لعضو** — عطّله (is_active=0) بدل الحذف
- **لا تضع effective_to = null في board offer** — مطلوب؛ العروض لازم لها نهاية - **لا تضع effective_to = null في board offer** — مطلوب دائماً
- **لا تنسى إن الفائدة سنوية مش شهرية** — 22% سنوي × (شهور/12) هي المعادلة - **لا تنسى إن الفائدة سنوية** — 22% سنوي × (أشهر/12)
- **لا تطبّق خصم لائحي بدون مستند** — حتى لو النظام يسمح، الاعتماد قد يُرفض
- **لا تتجاوز max_discount_percentage** — النظام يطبّقه تلقائياً
- **لا تنسى اعتماد طلبات الخصم اللائحي** — تبقى pending حتى يعتمدها مسؤول
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