# دور المندوب — توثيق التسليم

**المشروع:** تيكت سوريا · **التاريخ:** 2026-08-20 · **الحالة:** منشور على الإنتاج

---

## 1. ما الذي أُنجز

أُضيف دور `Representative` (مندوب) إلى النظام، يرى ويدير **فقط الشركات المُسنَدة إليه**، والتقييد مطبَّق في الـ Backend لا في الواجهة.

كما فُصل **الهامش التجاري** عن المندوب ووُضع على الشركة، بناءً على تصحيح المتطلبات: النسبة ليست عمولة للمندوب، بل ربح تيكت سوريا من شركة النقل.

---

## 2. قاعدة الملكية

```
Representative
      ↓  Company.RepresentativeId
   Company
      ↓
 ┌────┴────┐
Bus       Trip → Booking
```

الملكية محفوظة في **عمود واحد فقط**: `Company.RepresentativeId`.

الباصات والرحلات والحجوزات تُبلَغ عبر شركتها، فنقل شركة من مندوب إلى آخر ينقل شجرتها كاملة **بكتابة واحدة** دون تعديل أي باص أو رحلة.

**المحافظات نطاق عمل لا ملكية.** إسناد دمشق لمندوب لا يجعل شركات دمشق تابعة له تلقائياً، ويمكن أن يعمل مندوبان في نفس المحافظة دون تغيير البنية.

---

## 3. الجداول والحقول

### جداول جديدة

| الجدول | الغرض |
|---|---|
| `Representatives` | ملف المندوب، مرتبط بهوية الدخول الموحّدة `AppUser` |
| `Provinces` | المحافظات الأربع عشرة، مستخرجة من حقول `City.GovernorateAr` النصية |
| `RepresentativeProvinces` | علاقة many-to-many: نطاق عمل المندوب |
| `CompanyCommissionHistories` | سجل تدقيق لكل تغيير في هامش الشركة |

### أعمدة مضافة

**`Companies`**

| العمود | المعنى |
|---|---|
| `RepresentativeId` | المندوب المالك — `NULL` يعني شركة تابعة للمنصة مباشرة |
| `CreatedByUserId` | من أنشأ السجل، للتدقيق بعد النقل |
| `MarginType` | `percentage` أو `fixed` |
| `MarginAmount` | مبلغ ثابت لكل تذكرة، يُقرأ حين يكون النوع `fixed` |
| `CommissionSetByRepresentativeAt` | `NULL` = للمندوب إدخال واحد متبقٍّ |
| `CommissionSetByUserId` | آخر من غيّر الهامش |

**`Cities`** — `ProvinceId` (مفتاح أجنبي، والحقول النصية بقيت كما هي)

**`Bookings`** — `PlatformProfitAmount` و`PlatformProfitPercentage`

---

## 4. الهامش التجاري

الهامش **اتفاق بين تيكت سوريا وكل شركة**، وليس نسبة تخص المندوب. المندوب الواحد قد يدير شركات باتفاقيات مختلفة:

```
مندوب أحمد
  ├── النيل للنقل  → 10%
  ├── سوبر جيت     → 20 ليرة لكل تذكرة
  └── شركة الشام   → 6%
```

### طريقتان للاحتساب

**نسبة:** الربح = سعر البيع × النسبة ÷ 100

**مبلغ ثابت:** الربح = المبلغ × عدد التذاكر — للاتفاقات المصاغة هكذا: «الشركة تعطينا التذكرة بـ280 ونبيعها بـ300».

### من يحدد الهامش

| | الصلاحية |
|---|---|
| **المندوب** | إدخال واحد فقط، ما دام الهامش لم يُحدَّد بعد. بعده يصبح للقراءة فقط ويردّ الخادم **403** |
| **الأدمن** | تعديل غير محدود |

القيد مطبَّق في `CompanyMarginService` — كاتب واحد للهامش، فلا يمكن تجاوز القاعدة ولا سجل التدقيق من أي Controller.

> الشركات الست القائمة اعتُبرت **محددة مسبقاً** عند الترحيل، حتى لا يدهس مندوب جديد اتفاقاً قائماً بإدخاله الوحيد.

### Snapshot عند الحجز

تُحفَظ القيم لحظة إنشاء الحجز فلا تتأثر الحجوزات القديمة بأي تغيير لاحق:

| وقت الحجز | بعد تغيير الاتفاق |
|---|---|
| سعر البيع 300 | سعر البيع 300 |
| سعر الشركة 280 | سعر الشركة 290 |
| **الربح 20** | **الربح 10** |

الحجز القديم يبقى على 20، والجديد يأخذ 10.

### ربحنا حسب مصدر الحجز

| المصدر | ربح تيكت سوريا |
|---|---|
| **المنصة** — الزبون حجز عبر موقعنا | هامش الشركة المتفق عليه |
| **المكتب** — مكتب متعاقد يبيع عبر نظامنا | صفر |
| **الشركة** — بيع من لوحة الشركة نفسها | صفر |

`PlatformProfitAmount` يُسجَّل صفراً صراحةً في الحالتين الأخيرتين بدل تركه مبهماً. هذا مهم لأن `CommissionAmount` في حجوزات المكاتب يحمل **عمولة المكتب** لا ربحنا — ونصف حجوزات النظام مصدرها المكاتب.

---

## 5. الصلاحيات

نقطة التحكم واحدة: `CompanyScopedController.IsOwnCompanyAsync`، مستدعاة في **37 موضعاً** خلف `/api/company/{companyId}/*`. جعلُها تفهم المندوب غطّى دفعةً واحدة: الباصات، الرحلات، الجداول، الخطوط، الغراجات، المكاتب، الفواتير، ولوحة الشركة — بلا تكرار أي CRUD.

**التحقق يقرأ من قاعدة البيانات في كل طلب، لا من التوكن.** شركة نُقلت لمندوب آخر تفقد الوصول فوراً؛ التوكن القديم كان سيحمل قائمة قديمة حتى انتهاء صلاحيته.

### أمثلة مؤكَّدة بالاختبار

```
أحمد يملك الشركتين 1 و2 · محمد يملك الشركة 3

GET  /api/representative/companies/3        → 403
PUT  /api/representative/companies/3        → 403
GET  /api/company/3/{buses,trips,dashboard} → 403
POST /api/company/3/{buses,trips,routes}    → 403
GET  /api/admin/*                           → 403
نفس المسارات على شركاته هو                   → 200
```

### إنشاء شركة

حين ينشئ المندوب شركة يختم الخادم `RepresentativeId` من التوكن ويتجاهل أي قيمة في جسم الطلب. اختُبر بإرسال `representativeId` لمندوب آخر عمداً — حُفظت الشركة باسم المُرسِل.

### التعطيل لا الحذف

`IsActive = false` يوقف الدخول ويُبقي كل الشركات والباصات والرحلات، ويستطيع الأدمن نقل شركاته لمن يخلفه. التوكن القديم يفقد الصلاحية فوراً.

---

## 6. الواجهات

### لوحة المندوب — `admin.ticketsyria.com/representative`

لوحة التحكم · شركاتي (مع الإضافة وتحديد الهامش) · باصاتي · رحلاتي · المحافظات التابعة لي

لا تظهر له أي أقسام إدارة عامة، والـ API يرفضها أصلاً.

### قسم الأدمن — `admin.ticketsyria.com/admin/representatives`

| المندوب | المحافظات | الشركات | الحالة |
|---|---|---|---|
| أحمد | دمشق، ريف دمشق، درعا | 8 | فعال |
| محمد | حلب | 4 | فعال |

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

---

## 7. الترحيل

`20260820091646_AddRepresentativeRoleAndCompanyMargin` — **إضافية بالكامل: صفر عملية حذف**.

9 أعمدة جديدة + 4 جداول، ثم تعبئة آمنة تربط المدن بمحافظاتها وتنشئ محافظات لأي اسم أضافه الأدمن ولا تغطيه البذرة.

### النتيجة على قاعدة الإنتاج

| | قبل | بعد |
|---|---|---|
| الحجوزات | 62,599 | 62,599 |
| الرحلات | 14,978 | 14,978 |
| المدن | 97 | 97 |
| الشركات | 6 | 6 |

**checksums أعمدة الأموال متطابقة قبل وبعد.** المدن الـ97 رُبطت كلها بمحافظاتها ولا مدينة يتيمة. الحجوزات القديمة بقيت `PlatformProfitAmount = NULL` — لم أخترع Snapshot لم يُؤخذ.

اختُبر الترحيل على **نسخة كاملة من قاعدة الإنتاج** قبل تطبيقه، وأُخذت نسخة احتياطية قبل النشر.

---

## 8. الأعطال المكتشفة والمُصلحة

| العطل | السبب | الإصلاح |
|---|---|---|
| تغيير بريد المندوب يكسر الدخول | التغيير يمس ملف المندوب فقط، والدخول يتم عبر `AppUser`، فيتباعدان | مزامنة البريدين، ورفض أي بريد يملكه حساب دخول آخر |
| قَبول بريد يخص شخصاً آخر | فحص التفرّد كان بين المندوبين فقط لا بين حسابات الدخول | توسيع الفحص ليشمل `AppUsers` |
| الدخول كمندوب يحوّل لصفحة زبون | `/representative` لم يُسجَّل في `STAFF_PREFIXES`، فيُطرد من مضيف الإدارة إلى مضيف الزبائن — وهو أصل مختلف بتخزين مختلف، فتضيع الجلسة | تسجيل القسم ضمن أقسام الطاقم |
| الهامش غير قابل للتعديل من صفحة المندوب | كان رابطاً لصفحة الشركة لا حقلاً | زر يفتح محرّر الهامش مع سجله في مكانه |

---

## 9. بنود معلّقة

1. **البريد القديم يبقى صالحاً للدخول بعد تغييره** — مسار قديم في `AuthController` يتحقق من `Customers.Email` كبديل. يوصل لنفس الشخص لا لحساب آخر، لكن تضييقه يمس دخول كل المستخدمين فيحتاج قراراً صريحاً.
2. **كلمة مرور قاعدة الإنتاج مسرّبة** في تاريخ المستودع القديم وتحتاج تدويراً — بند سابق لهذا العمل.
3. **المندوب الحالي بلا شركات** — لوحته ستبقى أصفاراً حتى يُسنَد له شركة.

---

## 10. حساب الاختبار

```
admin.ticketsyria.com
jaberhasana104@gmail.com
```

الحساب يحمل دورين (زبون ومندوب) فتظهر شاشة اختيار الدور. **الدخول من مضيف الإدارة** — على موقع الزبائن تُخفى الأدوار الإدارية بحكم التصميم.
