# Business Rules — BookingForm API

> **الملف:** [api/models/BookingForm.php](BookingForm.php)
> **الغرض:** نموذج (Model) مسؤول عن إنشاء حجز جديد من خلال الـ API، سواء كان حجز خدمات منفردة أو حجز باقة (Package)، مع تطبيق كل القواعد الحسابية والخصومات.

---

## 1. نظرة عامة على الـ Flow

`BookingForm` بيستقبل البيانات الجاية من الـ API endpoint بتاع الحجز، وبيمر بالمراحل دي:

1. **Validation:** التحقق من صحة البيانات حسب الـ rules.
2. **Preparation:** حساب المبلغ، الضريبة، الخصومات (بروموكود + دعوة).
3. **Persist Booking:** حفظ الحجز في جدول `Booking`.
4. **Persist Booking Services:** حفظ الخدمات المرتبطة بالحجز.
5. **Persist Samples:** حفظ صور العينات لو موجودة.
6. **Update Invitations:** تحديث حالة دعوة المستخدم لو استخدم خصم الدعوة.

---

## 2. المدخلات (Input Fields)

| الحقل | النوع | إجباري؟ | الوصف |
|------|-------|----------|--------|
| `services_ids` | array | ✅ | IDs الخدمات اللي العميل بيحجزها |
| `shop_id` | integer | ❌ | ID المحل (بيتحدد تلقائي لو الخدمات كلها لنفس المحل) |
| `package_id` | integer | ❌ | ID الباقة (لو الحجز عبارة عن باقة) |
| `agent_id` | integer | ❌ | ID مقدم الخدمة |
| `samples_ids` | array | ❌ | IDs صور العينات المرفقة |
| `schedule_date` | string | ❌ | تاريخ الموعد |
| `from_hour` / `to_hour` | string | ❌ | الفترة الزمنية |
| `promo_code` | string | ❌ | كود الخصم |
| `invoice_id` | string | ❌ | رقم الفاتورة |

---

## 3. قواعد التحقق (Validation Rules)

- `services_ids` **إجباري دايمًا**.
- الحقول الرقمية (`shop_id`, `valid_code`, `booking_id`, `package_id`, `agent_id`, `promo_code_id`) لازم تكون أعداد صحيحة (integer).
- الحقول المالية (`amount`, `total_paid`, `promo_code_value`, `shop_discount_value`, `vat`) لازم تكون أرقام (numeric).
- لو الـ validation فشل → الحجز ميتعملش ويرجع `false`.

---

## 4. منطق الحجز بالخدمات (Services Booking)

### 4.1. الـ Method الرئيسية: `save()`

الـ flow الكامل:

1. التحقق من البيانات (`validate()`).
2. استدعاء `preparingWithPromoCode()` لتجهيز كل الحسابات.
3. **ضمان عدم وجود قيم سالبة** — كل القيم المالية بتمر على `max(0, ...)` كحماية.
4. إنشاء object جديد من `Booking` وملئه ببيانات المستخدم (موبايل، اسم، إيميل) من الـ identity.
5. حفظ الحجز.
6. لو فيه `invitation_discount` > 0 → تحديث `InvitesAvailability` للحالة `STATUS_USED`.
7. حفظ الخدمات المرتبطة عبر `preparingBookingServices()`.
8. حفظ صور العينات لو موجودة عبر `BookingHelper::preparingBookingSamples()`.

### 4.2. تجهيز الحسابات: `preparingWithPromoCode()`

#### أ. اختيار الخدمات الصحيحة
- بيستدعي `checkServices()` عشان يتأكد إن الـ services المختارة فيها agent (مقدم خدمة) واحد قادر يقدمها كلها.
- لو مفيش agent يقدر يقدم الـ list كاملة → بيدور على أكبر subset ممكن من الخدمات (راجع قسم 6).

#### ب. حساب المبلغ الإجمالي
- **AUTO-SUM with Duplicates:** الـ loop بيمر على كل ID في `services_ids` (مش على unique) — يعني لو نفس الخدمة اتطلبت مرتين، هتتحسب مرتين.
- المبلغ = مجموع `service_amount` لكل خدمة.

#### ج. حساب خصم الدعوة (Invitation Discount)
- بيستدعي `calculateInvitationDiscount($amount)`.

#### د. حساب VAT والمبلغ النهائي
- `vat = BookingHelper::vat(amount - invitation_discount)`
- `total_paid = BookingHelper::calculateTotalPaid(amount, 0, invitation_discount)`

#### هـ. تطبيق البروموكود (لو موجود)
- البروموكود لازم يستوفي الشروط دي مجتمعة:
  - `code` = الكود المدخل
  - `status` = `STATUS_ACTIVE`
  - `shop_id` = نفس shop الحجز
  - `expiry_date` > تاريخ النهاردة (بيتم التحويل من format `d/m/Y`)
- لو الكود غير صالح → `valid_code = NOT_VALID` (1).
- لو الكود صالح:
  - حساب قيمة الخصم عبر `discount_value()`.
  - **إعادة حساب** خصم الدعوة، VAT، والـ total_paid بناءً على المبلغ بعد البروموكود.
  - **مهم:** قيمة البروموكود بتتضرب في `(1 + vatRate)` عشان تشمل الضريبة → `promo_code_value = promo_code_value * (1 + vatRate)`.

---

## 5. منطق حجز الباقات (Package Booking)

### `preparingPackageWithPromoCode($package)`

- المبلغ مأخوذ مباشرة من `package->price`.
- بيتم استخراج كل الـ `services_ids` المرتبطة بالباقة من جدول `PackagesService`.
- حساب الـ VAT بطريقة مختلفة عن الخدمات: `ShopService::calculateVat($amount, $vatRate, $discountInclVat)`.
- المبلغ بيترجع `subtotal` (المبلغ بدون VAT).
- `shop_id` بيتحدد تلقائيًا من الباقة.
- نفس قواعد البروموكود وخصم الدعوة المطبقة في الخدمات.

---

## 6. خوارزمية اختيار الخدمات (Service Selection Algorithm)

### المشكلة
العميل ممكن يطلب 5 خدمات، لكن مفيش agent (مقدم خدمة) واحد قادر يقدم الـ 5 كلهم. الخوارزمية لازم تختار أكبر subset ممكن من الخدمات بحيث يكون فيه agent يقدر يقدمه.

### الـ Methods المتورطة:
- `checkServices()` — نقطة الدخول.
- `getUniqueServices()` / `allSubsets()` — توليد كل الـ subsets الممكنة من حجم معين.
- `processNewServicesList()` — فلترة الـ subsets اللي ليها agent متاح.
- `findHighestCostList()` — اختيار الـ subset الأعلى تكلفة.

### الخطوات:
1. **محاولة 1:** نشوف هل فيه agent يقدر يقدم كل الخدمات المطلوبة؟ لو نعم → ترجع الـ list كاملة.
2. لو لأ → نقلل الحجم بـ 1 ونجرب كل الـ subsets الممكنة من الحجم ده.
3. لو لقينا subset واحد بس عنده agent → ترجع.
4. لو لقينا أكتر من subset عنده agent → نختار الـ subset **الأعلى تكلفة** (`findHighestCostList`).
5. لو معلقناش حاجة → نقلل الحجم تاني ونعيد.

> **Business Rule:** الأولوية للمبلغ الأعلى عشان نعظم الإيرادات للمحل.

### مصدر بيانات الـ Agents
- جدول `UserShopService` بيربط الـ user (agent) بالخدمات اللي يقدر يقدمها.
- الـ query بتعمل GROUP BY على `user_id` و HAVING بتتأكد إن الـ user عنده عدد distinct services يساوي عدد الخدمات المطلوبة.

---

## 7. خصم الدعوة (Invitation Discount)

### `calculateInvitationDiscount($amount)`

**القاعدة:**
- بيدور على سجل في `InvitesAvailability` خاص بالمستخدم الحالي بشرط:
  - `user_id` = ID المستخدم
  - `status` = `STATUS_NEW` (لسه ما اتستخدمتش)
  - `available_to` > تاريخ النهاردة (لسه سارية)
- لو لقي → `invitation_discount = amount × percent / 100`
- لو ملقاش → `invitation_discount = 0`

### تحديث الحالة بعد الاستخدام
- في `save()`: لو `invitation_discount > 0` بعد الحفظ → الـ record بتاعته بياخد `status = STATUS_USED`.

> **Business Rule:** كل مستخدم عنده خصم دعوة واحد بس صالح في وقت معين.

---

## 8. حساب البروموكود (Promo Code Discount)

### `discount_value($type, $code_value, $total_service_amount)`

نوعان:
- **`TYPE_FIXED_AMOUNT`:** الخصم = القيمة المدخلة مباشرة (بمبلغ محدد).
- **النسبة المئوية:** الخصم = `total_service_amount × code_value / 100`.

> **مهم:** قيمة الخصم بعد كده بتتضرب في `(1 + vatRate)` لتشمل الضريبة.

---

## 9. التحقق من تطابق المحلات (`allServicesHaveSameShop`)

- بيتأكد إن كل الخدمات المختارة من **نفس المحل**.
- بيحسب المبلغ الصافي (بدون VAT) لكل خدمة:
  - `vat = (service_amount × vatRate) / (1 + vatRate)`
  - `net = service_amount - vat`
- لو الخدمات من محلات مختلفة → بيرجع `false` (الحجز ميتمش).
- لو من نفس المحل → `shop_id` بيتحدد تلقائيًا.

> **Business Rule:** ميصحش حجز واحد يحتوي على خدمات من محلات مختلفة.

---

## 10. حماية القيم السالبة (Defensive Logic)

كل القيم المالية بتمر على الفلتر ده في كل المراحل:

```php
$value = max(0, (float)($value ?: 0));
```

**السبب:** ضمان إنه ما يحصلش حالة فيها قيمة سالبة (لو مثلًا الخصم أكبر من المبلغ الأصلي) — في الحالة دي القيمة بتترد لـ 0.

> **Business Rule:** ولا قيمة مالية ممكن تكون سالبة في النظام.

---

## 11. حفظ الخدمات في الحجز (`preparingBookingServices`)

لكل خدمة في الـ list:
- بيتم تخزين السعر قبل الخصم (`service_amount_before`).
- السعر بعد الخصم (`service_amount`).
- قيمة الخصم على مستوى الخدمة (`discount_value` = الفرق بين الاتنين).
- السعر بدون VAT بعد الخصم (`price_excl_vat_after_discount`).
- الربط بـ `booking_id` و `service_id`.

---

## 12. الثوابت (Constants)

| الثابت | القيمة | الاستخدام |
|--------|--------|-----------|
| `NOT_VALID` | 1 | بيتحط على `valid_code` لما البروموكود يكون غير صالح |
| `TYPE_AMOUNT` | 1 | معرف لنوع الخصم بمبلغ ثابت |

---

## 13. التبعيات (Dependencies)

- **Models:** `Booking`, `BookingService`, `BookingSampleImage`, `ShopService`, `PackagesService`, `PromoCode`, `Invites`, `InvitesAvailability`, `UserShopService`, `Settings`, `User`.
- **Helpers:** `BookingHelper` (لحسابات VAT و total_paid), `ResponseHelper`.
- **Controllers:** `UserController` (للـ phone prefix).
- **Resources:** `PackagesResource`, `PackagesServiceResource`.

---

## 14. ملاحظات وفجوات (Issues / Gaps)

⚠️ **نقاط لازم تتراجع:**

1. **`var_dump($booking->errors)` في السطر 150** — كود debug متسيب في الـ production، المفروض يتشال.
2. **الكود اللي بعد `return true`** في السطر 145 (السطور 148-151) — مش بيتنفذ أبدًا لأن الـ method رجعت قبل كده. منطق ميت.
3. **Reading user every time:** `User::findOne(\Yii::$app->user->getId())` بيتعمل في `calculateInvitationDiscount` و `save` — ممكن يتعمل caching.
4. **`promo_code->save()`** بيتم استدعاؤه بدون أي تعديل على البروموكود — ملوش لازمة.
5. **Magic numbers:** `Settings::findOne(1)` بيستخدم ID ثابت — لازم يبقى ثابت في constants أو config.
6. **`getUniqueServices` و `allSubsets`** — recursion ممكن تكون expensive لو عدد الخدمات كبير (O(n!) في أسوأ حالة).
