تقرير فحص الهامش التجاري
تاريخ الفحص: 2026-08-20 · النطاق: فحص فقط — لم يُعدَّل أي كود أو قاعدة بيانات أو Migration
الخلاصة أولاً
النظام لا يخزّن سعر الشركة (280) إطلاقاً. ما يخزّنه هو سعر البيع (300) وربحنا (20)، ثم يشتقّ 280 بالطرح.
MarginAmount = 20 تعني ربحاً ثابتاً لتيكت سوريا مقداره 20 لكل تذكرة — لا «الفرق بين سعرين». الفرق أن الهامش الثابت مربوط بسعر البيع، لا بسعر الشركة، وهذا يُنتج سلوكاً قد لا يوافق اتفاقك. التفصيل في القسم 6.
1. كيف تُمثَّل حالة 280 → 300 حالياً؟
بالطريقة الأولى: MarginType = 'fixed' وMarginAmount = 20، والربح = 20 × عدد التذاكر.
لا يوجد سعر خاص بالشركة. الـ280 ليست مُدخَلاً بل ناتجاً يُحسب بعد الحجز:
CompanyNetAmount = TotalPrice − Profit = 300 − 20 = 280
قبل وجود أي حجز، الرقم 280 غير موجود في النظام. الموجود فقط: 300 في Trips.Price و20 في Companies.MarginAmount.
2. أين يُخزَّن سعر الـ280؟
غير مخزَّن كمُدخَل — لا يوجد أي عمود له في قاعدة البيانات
بحثتُ في مخطط القاعدة كاملاً عن أي عمود يحمل معنى سعر الشراء:
SELECT table_name||'.'||column_name FROM information_schema.columns
WHERE table_schema='public' AND (column_name ILIKE '%cost%' OR column_name ILIKE '%purchase%'
OR column_name ILIKE '%supplier%' OR column_name ILIKE '%buy%' OR column_name ILIKE '%companyprice%');
النتيجة: صفر صفوف.
أين يظهر الرقم 280 إذاً؟
| الجدول | Bookings |
| العمود | CompanyNetAmount — numeric(18,2), NOT NULL |
| الـ Entity | Booking.CompanyNetAmount |
| مكان الحساب | Services/BookingFinance.cs → ApplyPlatformMargin |
| السطر | booking.CompanyNetAmount = RoundMoney(booking.TotalPrice - profit); |
وهو ناتج محسوب ومُجمَّد لحظة الحجز، لا قيمة أدخلتها أنت.
CompanyPrice موجود أيضاً كاسم في AdminInvoicesController لكنه حقل DTO فقط يعكس CompanyNetAmount — ليس عموداً.
أين تُخزَّن الأرقام الثلاثة فعلاً
| الرقم | المكان | النوع |
|---|---|---|
| 300 سعر البيع | Trips.Price (تحدده الشركة عبر /api/company/{id}/trips) |
مُدخَل |
| 20 الهامش | Companies.MarginAmount + Companies.MarginType='fixed' |
مُدخَل |
| 280 سعر الشركة | لا مكان — يُشتق بالطرح بعد الحجز | ناتج |
3. مسار حساب PlatformProfitAmount
Trips.Price (300) + Seats.ExtraPrice
↓
BookingsController.Create → totalPrice = Σ (Trip.Price + ExtraPrice)
↓
booking.TotalPrice = totalPrice
↓
BookingFinance.ApplyPlatformMargin(
booking,
Company.MarginType, ← 'fixed' أو 'percentage'
Company.Commission, ← النسبة
Company.MarginAmount, ← المبلغ الثابت
request.Passengers.Count) ← عدد التذاكر
↓
booking.PlatformProfitAmount = profit
الفرق بين النمطين
fixed
var perTicket = Math.Max(0, marginAmountPerTicket);
profit = Math.Min(RoundMoney(perTicket * Math.Max(0, ticketCount)), booking.TotalPrice);
الربح = المبلغ × عدد التذاكر، مقصوصاً عند سعر البيع حتى لا تصبح حصة الشركة سالبة. لا يقرأ سعر البيع في الحساب إطلاقاً — فقط في القصّ.
percentage
var rate = Math.Max(0, commissionPercent);
profit = RoundMoney(booking.TotalPrice * rate / 100m);
الربح = سعر البيع × النسبة ÷ 100. يعتمد على سعر البيع مباشرة.
ثم في الحالتين معاً
var effectiveRate = booking.TotalPrice > 0 ? RoundMoney(profit * 100m / booking.TotalPrice) : 0;
booking.CommissionPercentage = effectiveRate;
booking.CommissionAmount = profit;
booking.CompanyNetAmount = RoundMoney(booking.TotalPrice - profit); // ← 280 يُولد هنا
booking.PlatformProfitAmount = profit;
booking.PlatformProfitPercentage = effectiveRate;
التقريب: Math.Round(value, 2, MidpointRounding.AwayFromZero).
النسبة المكافئة تُشتق حتى للاتفاق الثابت، فتقرأ التقارير عموداً واحداً مهما كانت صياغة الاتفاق.
4. ماذا تعني MarginAmount بالضبط؟
تعني: ربح ثابت لتيكت سوريا مقداره 20 لكل تذكرة
لا تعني «الفرق بين سعر الشركة وسعر البيع» — لأنه لا يوجد سعر شركة مخزَّن لتُطرح منه.
الرقمان يتطابقان فقط حين يكون سعر البيع المدفوع فعلاً 300 بالضبط. وفي غير ذلك يفترقان:
| الحالة | سعر البيع | ربحنا | حصة الشركة | سعر الشركة الضمني |
|---|---|---|---|---|
| الحالة المتوقعة | 300 | 20 | 280 | 280 ✅ |
| مقعد VIP بإضافة 50 | 350 | 20 | 330 | 330 ← تغيّر وحده |
| الشركة رفعت السعر إلى 320 | 320 | 20 | 300 | 300 ← تغيّر وحده |
| حجز مقطع قصير بأجرة 15 | 15 | 15 | 0 | 0 ← قُصَّ |
في كل صفّ الهامش بقي 20، لكن «سعر الشركة» تحرّك. لأن الثابت هو ربحنا، لا سعر الشركة.
5. فحص الـ Snapshot
ما يحفظه الحجز فعلاً
أعمدة Bookings المالية كاملةً كما هي في قاعدة الإنتاج:
| العمود | النوع | يحفظ |
|---|---|---|
TotalPrice |
numeric(18,2) NOT NULL |
سعر البيع وقت الحجز ✅ |
CompanyNetAmount |
numeric(18,2) NOT NULL |
سعر الشركة وقت الحجز ✅ |
PlatformProfitAmount |
numeric(18,2) NULL |
ربحنا ✅ |
PlatformProfitPercentage |
numeric(5,2) NULL |
النسبة المكافئة ✅ |
CommissionPercentage |
numeric(5,2) NOT NULL |
نسخة من النسبة |
CommissionAmount |
numeric(18,2) NOT NULL |
نسخة من الربح |
OfficeProfitPercentage |
numeric(5,2) NULL |
عمولة المكتب |
الجواب على سؤالك
إذا تغير سعر الشركة أو الاتفاق لاحقاً، هل يمكننا من الحجز القديم معرفة 280 و300 و20؟
نعم، الثلاثة كلها. لكن بأسماء مختلفة عمّا في تقرير التسليم:
سعر البيع وقت الحجز = Booking.TotalPrice = 300
سعر الشركة وقت الحجز = Booking.CompanyNetAmount = 280
الربح = Booking.PlatformProfitAmount = 20
القيم مُجمَّدة لحظة الإنشاء ولا يعيد أي كود كتابتها.
ما لا يحفظه الحجز
| مطلوب في سؤالك | موجود؟ |
|---|---|
CompanyPriceSnapshot |
❌ بالاسم — لكن CompanyNetAmount يؤدي وظيفته |
SellingPriceSnapshot |
❌ بالاسم — لكن TotalPrice يؤدي وظيفته |
MarginTypeSnapshot |
❌ غير موجود إطلاقاً |
MarginValueSnapshot |
❌ غير موجود إطلاقاً |
PlatformProfitAmount |
✅ موجود |
الفجوة الحقيقية: من صفّ الحجز وحده لا تستطيع معرفة هل كان الاتفاق «20 ثابت» أم «6.67%». تحصل على النسبة المكافئة 6.67% في الحالتين، ولا تميّز بينهما.
تخفيف جزئي: جدول CompanyCommissionHistories يسجّل كل تغيير مع OldMarginType/OldCommission/OldMarginAmount وChangedAt، فيمكن استنتاج الاتفاق السائد وقت الحجز بمطابقة Booking.CreatedAt مع تاريخ التغييرات. لكنه استنتاج بربط زمني، لا حقيقة مثبتة في الصف نفسه. (الجدول فارغ حالياً — صفر سجلات.)
6. اختبار السيناريو الذي طلبته
اليوم
Company Price = 280 · Selling = 300 · Tickets = 2 · المتوقع = 40
التنفيذ الحالي: MarginType='fixed', MarginAmount=20 → الربح = 20 × 2 = 40 ✅ · حصة الشركة = 600 − 40 = 560 = 2 × 280 ✅
بعد تغيّر الاتفاق
Company Price = 290 · Selling = 300 · Tickets = 2 · المتوقع = 20
التنفيذ الحالي لا يستطيع تمثيل هذا كسعر شركة متغيّر. لا يوجد حقل تكتب فيه 290.
يجب تغيير MarginAmount يدوياً من 20 إلى 10. حينها: الربح = 10 × 2 = 20 ✅
وهنا الخطر الحقيقي
الهامش الثابت مربوط بسعر البيع لا بسعر الشركة. وTrips.Price تحدده الشركة نفسها من لوحتها. فإن رفعت الشركة سعر البيع:
| سعر البيع | ربحنا | حصة الشركة | |
|---|---|---|---|
| قبل | 600 (2×300) | 40 | 560 |
| بعد رفع السعر إلى 320 | 640 | 40 | 600 |
الزيادة كاملةً (+40) ذهبت للشركة، وربحنا لم يتحرك. لو كان الاتفاق مُمثَّلاً كسعر شركة (280 ثابت) لكان ربحنا 80.
عملياً: تستطيع الشركة رفع سعر البيع وتحتفظ بالفارق كله دون أن يتغيّر ما نقبضه، ودون أن ينبّه النظام أحداً.
بالمقابل، نمط percentage محصَّن من ذلك — لأن الربح نسبة من سعر البيع فيتحرك معه تلقائياً.
7. الخلاصة بالصيغة المطلوبة
Current implementation:
Company purchase price stored? NO
Where: لا يوجد عمود له في المخطط كله. يُشتق فقط بعد الحجز في
Bookings.CompanyNetAmount = TotalPrice − Profit
Selling price stored? YES
Where: Trips.Price (تحدده الشركة)
+ Seats.ExtraPrice
→ مُجمَّد في Bookings.TotalPrice وقت الحجز
Fixed margin means: ربح ثابت لتيكت سوريا لكل تذكرة.
Profit = MarginAmount × TicketCount
(مقصوص عند TotalPrice). ليس فرقاً بين سعرين.
Percentage margin calculated from: سعر البيع.
Profit = Bookings.TotalPrice × Companies.Commission / 100
Booking stores company price snapshot? YES (Bookings.CompanyNetAmount — محسوب لا مُدخَل)
Booking stores selling price snapshot? YES (Bookings.TotalPrice)
Booking stores margin snapshot? PARTIAL
✅ النتيجة: PlatformProfitAmount + PlatformProfitPercentage
❌ الاتفاق نفسه: لا MarginType ولا MarginValue
→ لا يمكن التمييز بين «20 ثابت» و«6.67%» من الصف
Booking stores final platform profit? YES (Bookings.PlatformProfitAmount)
280 → 300 is currently represented as:
MarginType = 'fixed', MarginAmount = 20
Profit = 20 × TicketCount
الـ280 ناتج بالطرح، وليست مُدخَلاً في أي مكان.
Potential issue, if any:
1. الهامش الثابت مربوط بسعر البيع لا بسعر الشركة. والشركة تتحكم بسعر البيع
من لوحتها، فأي رفع للسعر يذهب بالكامل إليها وربحنا لا يتغيّر.
2. تغيّر سعر الشركة (280→290) يتطلب حساباً يدوياً وتعديلاً يدوياً
لـ MarginAmount (20→10). النظام لا يقبل سعر الشركة كمُدخَل.
3. Seats.ExtraPrice يرفع سعر البيع دون أن يرفع الهامش الثابت،
فيزيد «سعر الشركة الضمني» تلقائياً.
4. حجوزات المقاطع قد تكون أرخص من الهامش الثابت، فيُقصّ الربح عند
سعر البيع وتصبح حصة الشركة صفراً (مثال: أجرة 15 وهامش 20).
5. نوع الاتفاق غير محفوظ في الحجز، فالتدقيق اللاحق يحتاج ربطاً زمنياً
مع CompanyCommissionHistories بدل قراءة الصف مباشرة.
ملاحظة على الوضع الحالي
الشركات الست كلها على percentage (7–12%) وMarginAmount = 0. لم يُستخدم النمط الثابت على الإنتاج بعد، وكل الحجوزات الـ62,599 سابقة لعمود PlatformProfitAmount (كلها NULL).
أي أن النقاط أعلاه مخاطر مستقبلية لم تقع بعد، ويمكن تفاديها قبل أول استخدام فعلي للنمط الثابت.
لم أُجرِ أي تعديل. جاهز لمناقشة الخيارات إن رأيت أن التنفيذ يحتاج تغييراً.