دور المندوب — توثيق التسليم
المشروع: تيكت سوريا · التاريخ: 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 لا من موقع الزبائن — الأدوار الإدارية مخفيّة هناك بحكم التصميم. والحساب الذي يحمل دورين تظهر له شاشة اختيار الدور أولاً.
6.1 أقسام قائمته الجانبية — /representative
عنوان اللوحة «لوحة المندوب»، وفيها خمسة أقسام لا غير:
| القسم | المسار | ما يظهر فيه |
|---|---|---|
| لوحة التحكم | /representative |
ست بطاقات أرقام + شريط محافظاته |
| شركاتي | /representative/companies |
جدول شركاته · بحث · «إضافة شركة» · زر الهامش |
| باصاتي | /representative/buses |
باصات شركاته، بعد اختيار شركة من قائمة |
| رحلاتي | /representative/trips |
رحلات شركاته، بعد اختيار شركة من قائمة |
| المحافظات التابعة لي | /representative/provinces |
بطاقة لكل محافظة وعدد مدنها — قراءة فقط |
على الجوال شريط سفلي من خمسة: الرئيسية · شركاتي · رحلاتي · محافظاتي · المزيد.
6.2 بطاقات لوحة التحكم
| البطاقة | ماذا تعدّ | تفتح على |
|---|---|---|
| شركاتي | شركاته | /representative/companies |
| الباصات | باصات شركاته | /representative/buses |
| الرحلات | كل رحلات شركاته | /representative/trips |
| رحلات قادمة | المجدولة من اليوم فصاعداً | /representative/trips |
| الحجوزات | حجوزات رحلات شركاته | /representative/companies — لا صفحة حجوزات له |
| محافظاتي | محافظاته المُسنَدة | /representative/provinces |
الأرقام الستة تُحسب في الاستعلام نفسه مقيَّدةً بـcompanyIds المملوكة — لا تصفية في المتصفح يمكن تجاوزها.
تحت البطاقات شريط بأسماء محافظاته، وإن لم تُسنَد إليه بعد يظهر: «لم تُسنَد إليك محافظات بعد — يحددها الأدمن».
6.3 أعمدة جداوله والإجراءات المتاحة فيها
| الشاشة | الأعمدة | الإجراءات |
|---|---|---|
| شركاتي | الشركة · المدينة · الهاتف · الحالة · الباصات · الرحلات · هامش تيكت سوريا | إضافة شركة · الباصات · الرحلات · الكراجات · نافذة الهامش |
| باصاتي | الشركة · النمرة · النوع · التوزيع · عدد المقاعد · أُضيف | إضافة باص للشركة المختارة |
| رحلاتي | الشركة · من · إلى · التاريخ · الإقلاع · الوصول · المقاعد · السعر · الحالة | إضافة رحلة للشركة المختارة |
زر الهامش في «شركاتي» يعرض القيمة الحالية (10% أو 20 / تذكرة)، ويحمل شارة «لم يُحدَّد» ما دام إدخاله الوحيد متاحاً. بعد الحفظ يُفتح النموذج مقفلاً بنص يحيل إلى الإدارة — تفصيل القاعدة في القسم 4.
6.4 داخل الشركة الواحدة — /representative/companies/{id}/…
| القسم | ما يستطيعه |
|---|---|
| الباصات | عرض · إضافة · تعديل · تفاصيل |
| الكراجات | عرض · تفاصيل |
| الخطوط | عرض وإدارة خطوط الشركة |
| الرحلات | عرض · إنشاء · تعديل · تفاصيل |
| مخطط المقاعد | عرض المقاعد وحجز يدوي باسم الشركة |
هذه ليست نسخة ثانية من شاشات لوحة الشركة، بل الشاشات نفسها معاد تصديرها بمسار أساس مختلف خلف بوابة OwnedCompanyGate — فأي تحسين على شاشة الشركة يصل المندوب في اللحظة ذاتها، ولا يوجد مساران للكود يفترقان مع الوقت.
6.5 ما لا يظهر له
| الغائب | التفصيل |
|---|---|
| أقسام الأدمن السبعة عشر | الحسابات · المدفوعات · الاستردادات · الزبائن · المدن · الإشعارات · الإعدادات · المندوبون الآخرون… لا رابط لها، والـ API يردّ 403 لو طُلبت مباشرة |
| الحجوزات · التقارير · الإعدادات · سياسة الإلغاء · شاشة بيع المقاعد | أقسام في لوحة الشركة بلا مقابل تحت /representative |
| الفواتير · المكاتب | فجوة واجهة لا فجوة صلاحية: لا صفحة له، مع أن CompanyScopedController يقبل طلبه عليها لشركاته — بند مفتوح إن أُريد له إدارتها |
حارس الواجهة RepresentativeGuard يحوّل غير المندوب إلى /login، وهو راحة عرض لا حماية: كل نقطة نهاية خلف هذه الشاشات تعيد التحقق في الخادم، فتجاوز الحارس لا يسرّب شيئاً.
6.6 نقاط النهاية خلف هذه الشاشات
الكونترولر كله خلف [Authorize(Roles = "Representative")]، وكل دالة فيه تعيد التحقق من الملكية وتردّ 403 لا 404 — الصف موجود، والمُطالِب لا حق له فيه.
| المسار | الغرض |
|---|---|
GET /api/representative/me |
ملفه · محافظاته · شركاته |
GET /api/representative/dashboard |
الأرقام الستة |
GET /api/representative/companies |
شركاته — لا يُرسل أي معرّف، الخادم يشتقه من التوكن |
GET · POST · PUT /api/representative/companies[/{id}] |
عرض · إنشاء · تعديل |
GET · PUT /api/representative/companies/{id}/margin |
الهامش وسجل تغييره |
GET /api/representative/provinces |
محافظاته |
/api/company/{id}/{buses,trips,trip-schedules,routes,garages,offices,invoices} |
لشركاته وحدها، عبر IsOwnCompanyAsync |
قسم الأدمن — 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. بنود معلّقة
- البريد القديم يبقى صالحاً للدخول بعد تغييره — مسار قديم في
AuthControllerيتحقق منCustomers.Emailكبديل. يوصل لنفس الشخص لا لحساب آخر، لكن تضييقه يمس دخول كل المستخدمين فيحتاج قراراً صريحاً. - كلمة مرور قاعدة الإنتاج مسرّبة في تاريخ المستودع القديم وتحتاج تدويراً — بند سابق لهذا العمل.
- المندوب الحالي بلا شركات — لوحته ستبقى أصفاراً حتى يُسنَد له شركة.
- الفواتير والمكاتب بلا واجهة للمندوب — الـ API يقبل طلبه عليهما لشركاته، ولا صفحة تحت
/representativeتستعملهما. القرار مطلوب: إظهارهما أو تضييق الصلاحية لتطابق الواجهة.
10. حساب الاختبار
admin.ticketsyria.com
jaberhasana104@gmail.com
الحساب يحمل دورين (زبون ومندوب) فتظهر شاشة اختيار الدور. الدخول من مضيف الإدارة — على موقع الزبائن تُخفى الأدوار الإدارية بحكم التصميم.