# تقرير فحص الهامش التجاري

**تاريخ الفحص:** 2026-08-20 · **النطاق:** فحص فقط — لم يُعدَّل أي كود أو قاعدة بيانات أو Migration

---

## الخلاصة أولاً

النظام **لا يخزّن سعر الشركة (280) إطلاقاً**. ما يخزّنه هو سعر البيع (300) وربحنا (20)، ثم يشتقّ 280 بالطرح.

`MarginAmount = 20` تعني **ربحاً ثابتاً لتيكت سوريا مقداره 20 لكل تذكرة** — لا «الفرق بين سعرين». الفرق أن الهامش الثابت **مربوط بسعر البيع، لا بسعر الشركة**، وهذا يُنتج سلوكاً قد لا يوافق اتفاقك. التفصيل في القسم 6.

---

## 1. كيف تُمثَّل حالة 280 → 300 حالياً؟

**بالطريقة الأولى:** `MarginType = 'fixed'` و`MarginAmount = 20`، والربح = `20 × عدد التذاكر`.

**لا يوجد سعر خاص بالشركة.** الـ280 ليست مُدخَلاً بل **ناتجاً** يُحسب بعد الحجز:

```
CompanyNetAmount = TotalPrice − Profit = 300 − 20 = 280
```

قبل وجود أي حجز، الرقم 280 **غير موجود في النظام**. الموجود فقط: 300 في `Trips.Price` و20 في `Companies.MarginAmount`.

---

## 2. أين يُخزَّن سعر الـ280؟

### غير مخزَّن كمُدخَل — لا يوجد أي عمود له في قاعدة البيانات

بحثتُ في مخطط القاعدة كاملاً عن أي عمود يحمل معنى سعر الشراء:

```sql
SELECT table_name||'.'||column_name FROM information_schema.columns
WHERE table_schema='public' AND (column_name ILIKE '%cost%' OR column_name ILIKE '%purchase%'
   OR column_name ILIKE '%supplier%' OR column_name ILIKE '%buy%' OR column_name ILIKE '%companyprice%');
```

**النتيجة: صفر صفوف.**

### أين يظهر الرقم 280 إذاً؟

| | |
|---|---|
| **الجدول** | `Bookings` |
| **العمود** | `CompanyNetAmount` — `numeric(18,2)`, NOT NULL |
| **الـ Entity** | `Booking.CompanyNetAmount` |
| **مكان الحساب** | `Services/BookingFinance.cs` → `ApplyPlatformMargin` |
| **السطر** | `booking.CompanyNetAmount = RoundMoney(booking.TotalPrice - profit);` |

وهو **ناتج محسوب ومُجمَّد لحظة الحجز**، لا قيمة أدخلتها أنت.

`CompanyPrice` موجود أيضاً كاسم في `AdminInvoicesController` لكنه **حقل DTO فقط** يعكس `CompanyNetAmount` — ليس عموداً.

### أين تُخزَّن الأرقام الثلاثة فعلاً

| الرقم | المكان | النوع |
|---|---|---|
| **300** سعر البيع | `Trips.Price` (تحدده **الشركة** عبر `/api/company/{id}/trips`) | مُدخَل |
| **20** الهامش | `Companies.MarginAmount` + `Companies.MarginType='fixed'` | مُدخَل |
| **280** سعر الشركة | لا مكان — يُشتق بالطرح بعد الحجز | ناتج |

---

## 3. مسار حساب `PlatformProfitAmount`

```
Trips.Price (300) + Seats.ExtraPrice
        ↓
BookingsController.Create → totalPrice = Σ (Trip.Price + ExtraPrice)
        ↓
booking.TotalPrice = totalPrice
        ↓
BookingFinance.ApplyPlatformMargin(
    booking,
    Company.MarginType,        ← 'fixed' أو 'percentage'
    Company.Commission,        ← النسبة
    Company.MarginAmount,      ← المبلغ الثابت
    request.Passengers.Count)  ← عدد التذاكر
        ↓
booking.PlatformProfitAmount = profit
```

### الفرق بين النمطين

**`fixed`**

```csharp
var perTicket = Math.Max(0, marginAmountPerTicket);
profit = Math.Min(RoundMoney(perTicket * Math.Max(0, ticketCount)), booking.TotalPrice);
```

الربح = المبلغ × عدد التذاكر، **مقصوصاً عند سعر البيع** حتى لا تصبح حصة الشركة سالبة. **لا يقرأ سعر البيع في الحساب إطلاقاً** — فقط في القصّ.

**`percentage`**

```csharp
var rate = Math.Max(0, commissionPercent);
profit = RoundMoney(booking.TotalPrice * rate / 100m);
```

الربح = سعر البيع × النسبة ÷ 100. **يعتمد على سعر البيع مباشرة.**

### ثم في الحالتين معاً

```csharp
var effectiveRate = booking.TotalPrice > 0 ? RoundMoney(profit * 100m / booking.TotalPrice) : 0;

booking.CommissionPercentage     = effectiveRate;
booking.CommissionAmount         = profit;
booking.CompanyNetAmount         = RoundMoney(booking.TotalPrice - profit);   // ← 280 يُولد هنا
booking.PlatformProfitAmount     = profit;
booking.PlatformProfitPercentage = effectiveRate;
```

التقريب: `Math.Round(value, 2, MidpointRounding.AwayFromZero)`.

النسبة المكافئة تُشتق حتى للاتفاق الثابت، فتقرأ التقارير عموداً واحداً مهما كانت صياغة الاتفاق.

---

## 4. ماذا تعني `MarginAmount` بالضبط؟

### تعني: ربح ثابت لتيكت سوريا مقداره 20 لكل تذكرة

**لا تعني** «الفرق بين سعر الشركة وسعر البيع» — لأنه لا يوجد سعر شركة مخزَّن لتُطرح منه.

الرقمان يتطابقان **فقط** حين يكون سعر البيع المدفوع فعلاً 300 بالضبط. وفي غير ذلك يفترقان:

| الحالة | سعر البيع | ربحنا | حصة الشركة | سعر الشركة الضمني |
|---|---|---|---|---|
| الحالة المتوقعة | 300 | 20 | 280 | **280** ✅ |
| مقعد VIP بإضافة 50 | 350 | 20 | 330 | **330** ← تغيّر وحده |
| الشركة رفعت السعر إلى 320 | 320 | 20 | 300 | **300** ← تغيّر وحده |
| حجز مقطع قصير بأجرة 15 | 15 | 15 | 0 | **0** ← قُصَّ |

في كل صفّ الهامش بقي 20، لكن «سعر الشركة» تحرّك. لأن الثابت هو **ربحنا**، لا سعر الشركة.

---

## 5. فحص الـ Snapshot

### ما يحفظه الحجز فعلاً

أعمدة `Bookings` المالية كاملةً كما هي في قاعدة الإنتاج:

| العمود | النوع | يحفظ |
|---|---|---|
| `TotalPrice` | `numeric(18,2)` NOT NULL | **سعر البيع وقت الحجز** ✅ |
| `CompanyNetAmount` | `numeric(18,2)` NOT NULL | **سعر الشركة وقت الحجز** ✅ |
| `PlatformProfitAmount` | `numeric(18,2)` NULL | **ربحنا** ✅ |
| `PlatformProfitPercentage` | `numeric(5,2)` NULL | النسبة المكافئة ✅ |
| `CommissionPercentage` | `numeric(5,2)` NOT NULL | نسخة من النسبة |
| `CommissionAmount` | `numeric(18,2)` NOT NULL | نسخة من الربح |
| `OfficeProfitPercentage` | `numeric(5,2)` NULL | عمولة المكتب |

### الجواب على سؤالك

> إذا تغير سعر الشركة أو الاتفاق لاحقاً، هل يمكننا من الحجز القديم معرفة 280 و300 و20؟

**نعم، الثلاثة كلها.** لكن بأسماء مختلفة عمّا في تقرير التسليم:

```
سعر البيع وقت الحجز = Booking.TotalPrice        = 300
سعر الشركة وقت الحجز = Booking.CompanyNetAmount  = 280
الربح               = Booking.PlatformProfitAmount = 20
```

القيم مُجمَّدة لحظة الإنشاء ولا يعيد أي كود كتابتها.

### ما لا يحفظه الحجز

| مطلوب في سؤالك | موجود؟ |
|---|---|
| `CompanyPriceSnapshot` | ❌ بالاسم — لكن `CompanyNetAmount` يؤدي وظيفته |
| `SellingPriceSnapshot` | ❌ بالاسم — لكن `TotalPrice` يؤدي وظيفته |
| `MarginTypeSnapshot` | ❌ **غير موجود إطلاقاً** |
| `MarginValueSnapshot` | ❌ **غير موجود إطلاقاً** |
| `PlatformProfitAmount` | ✅ موجود |

**الفجوة الحقيقية:** من صفّ الحجز وحده لا تستطيع معرفة **هل كان الاتفاق «20 ثابت» أم «6.67%»**. تحصل على النسبة المكافئة 6.67% في الحالتين، ولا تميّز بينهما.

**تخفيف جزئي:** جدول `CompanyCommissionHistories` يسجّل كل تغيير مع `OldMarginType/OldCommission/OldMarginAmount` و`ChangedAt`، فيمكن استنتاج الاتفاق السائد وقت الحجز بمطابقة `Booking.CreatedAt` مع تاريخ التغييرات. لكنه استنتاج بربط زمني، لا حقيقة مثبتة في الصف نفسه. (الجدول فارغ حالياً — صفر سجلات.)

---

## 6. اختبار السيناريو الذي طلبته

### اليوم

```
Company Price = 280 · Selling = 300 · Tickets = 2 · المتوقع = 40
```

**التنفيذ الحالي:** `MarginType='fixed'`, `MarginAmount=20` → الربح = 20 × 2 = **40** ✅ · حصة الشركة = 600 − 40 = 560 = 2 × 280 ✅

### بعد تغيّر الاتفاق

```
Company Price = 290 · Selling = 300 · Tickets = 2 · المتوقع = 20
```

**التنفيذ الحالي لا يستطيع تمثيل هذا كسعر شركة متغيّر.** لا يوجد حقل تكتب فيه 290.

**يجب تغيير `MarginAmount` يدوياً من 20 إلى 10.** حينها: الربح = 10 × 2 = 20 ✅

### وهنا الخطر الحقيقي

الهامش الثابت **مربوط بسعر البيع لا بسعر الشركة**. و`Trips.Price` **تحدده الشركة نفسها** من لوحتها. فإن رفعت الشركة سعر البيع:

| | سعر البيع | ربحنا | حصة الشركة |
|---|---|---|---|
| قبل | 600 (2×300) | 40 | 560 |
| بعد رفع السعر إلى 320 | 640 | **40** | **600** |

**الزيادة كاملةً (+40) ذهبت للشركة، وربحنا لم يتحرك.** لو كان الاتفاق مُمثَّلاً كسعر شركة (280 ثابت) لكان ربحنا 80.

عملياً: تستطيع الشركة رفع سعر البيع وتحتفظ بالفارق كله دون أن يتغيّر ما نقبضه، ودون أن ينبّه النظام أحداً.

بالمقابل، نمط `percentage` محصَّن من ذلك — لأن الربح نسبة من سعر البيع فيتحرك معه تلقائياً.

---

## 7. الخلاصة بالصيغة المطلوبة

```text
Current implementation:

Company purchase price stored?  NO
Where:  لا يوجد عمود له في المخطط كله. يُشتق فقط بعد الحجز في
        Bookings.CompanyNetAmount = TotalPrice − Profit

Selling price stored?  YES
Where:  Trips.Price  (تحدده الشركة)
        + Seats.ExtraPrice
        → مُجمَّد في Bookings.TotalPrice وقت الحجز

Fixed margin means:  ربح ثابت لتيكت سوريا لكل تذكرة.
                     Profit = MarginAmount × TicketCount
                     (مقصوص عند TotalPrice). ليس فرقاً بين سعرين.

Percentage margin calculated from:  سعر البيع.
                     Profit = Bookings.TotalPrice × Companies.Commission / 100

Booking stores company price snapshot?    YES  (Bookings.CompanyNetAmount — محسوب لا مُدخَل)
Booking stores selling price snapshot?    YES  (Bookings.TotalPrice)
Booking stores margin snapshot?           PARTIAL
                     ✅ النتيجة: PlatformProfitAmount + PlatformProfitPercentage
                     ❌ الاتفاق نفسه: لا MarginType ولا MarginValue
                     → لا يمكن التمييز بين «20 ثابت» و«6.67%» من الصف
Booking stores final platform profit?     YES  (Bookings.PlatformProfitAmount)

280 → 300 is currently represented as:
                     MarginType = 'fixed', MarginAmount = 20
                     Profit = 20 × TicketCount
                     الـ280 ناتج بالطرح، وليست مُدخَلاً في أي مكان.

Potential issue, if any:
  1. الهامش الثابت مربوط بسعر البيع لا بسعر الشركة. والشركة تتحكم بسعر البيع
     من لوحتها، فأي رفع للسعر يذهب بالكامل إليها وربحنا لا يتغيّر.
  2. تغيّر سعر الشركة (280→290) يتطلب حساباً يدوياً وتعديلاً يدوياً
     لـ MarginAmount (20→10). النظام لا يقبل سعر الشركة كمُدخَل.
  3. Seats.ExtraPrice يرفع سعر البيع دون أن يرفع الهامش الثابت،
     فيزيد «سعر الشركة الضمني» تلقائياً.
  4. حجوزات المقاطع قد تكون أرخص من الهامش الثابت، فيُقصّ الربح عند
     سعر البيع وتصبح حصة الشركة صفراً (مثال: أجرة 15 وهامش 20).
  5. نوع الاتفاق غير محفوظ في الحجز، فالتدقيق اللاحق يحتاج ربطاً زمنياً
     مع CompanyCommissionHistories بدل قراءة الصف مباشرة.
```

---

## ملاحظة على الوضع الحالي

الشركات الست كلها على `percentage` (7–12%) و`MarginAmount = 0`. **لم يُستخدم النمط الثابت على الإنتاج بعد**، وكل الحجوزات الـ62,599 سابقة لعمود `PlatformProfitAmount` (كلها `NULL`).

أي أن النقاط أعلاه **مخاطر مستقبلية لم تقع بعد**، ويمكن تفاديها قبل أول استخدام فعلي للنمط الثابت.

لم أُجرِ أي تعديل. جاهز لمناقشة الخيارات إن رأيت أن التنفيذ يحتاج تغييراً.
