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

المشروع: تيكت سوريا · التاريخ: 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 آخر من غيّر الهامش

CitiesProvinceId (مفتاح أجنبي، والحقول النصية بقيت كما هي)

BookingsPlatformProfitAmount و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

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