دور المندوب — توثيق التسليم
المشروع: تيكت سوريا · التاريخ: 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. بنود معلّقة
- البريد القديم يبقى صالحاً للدخول بعد تغييره — مسار قديم في
AuthControllerيتحقق منCustomers.Emailكبديل. يوصل لنفس الشخص لا لحساب آخر، لكن تضييقه يمس دخول كل المستخدمين فيحتاج قراراً صريحاً. - كلمة مرور قاعدة الإنتاج مسرّبة في تاريخ المستودع القديم وتحتاج تدويراً — بند سابق لهذا العمل.
- المندوب الحالي بلا شركات — لوحته ستبقى أصفاراً حتى يُسنَد له شركة.
10. حساب الاختبار
admin.ticketsyria.com
jaberhasana104@gmail.com
الحساب يحمل دورين (زبون ومندوب) فتظهر شاشة اختيار الدور. الدخول من مضيف الإدارة — على موقع الزبائن تُخفى الأدوار الإدارية بحكم التصميم.