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

المشروع: تيكت سوريا · التاريخ: 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. بنود معلّقة

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

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

admin.ticketsyria.com
jaberhasana104@gmail.com

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