نظام الحجوزات
الحجز هو الكيان المركزي. جميع منطق الأعمال يدور حول إنشاء وإدارة وتتبع الحجوزات عبر 4 قنوات مع آلة حالة مُطبّقة على مستوى قاعدة البيانات.
آلة حالة الحجز
كل حجز يمر عبر مجموعة محددة من الحالات. الانتقالات تُفرض في كود التطبيق وتُتحقق بواسطة عمود booking_status (enum مخزّن كـ string عبر HasConversion<string>() في EF Core، مع قيد CHECK على مستوى قاعدة البيانات).
4 قنوات للحجز
تدخل الحجوزات إلى النظام عبر أربع قنوات مختلفة، لكل منها تدفقات مستخدم وخصائص أعمال مختلفة.
قناة المسؤول 70.6% من الحجم
المشغّلون ينشئون حجوزات للعملاء الحاضرين والمتصلين هاتفياً عبر لوحة تحكم الويب. هذه القناة هي المهيمنة حالياً وستظل عالية الحجم حتى مع نمو الحجز الذاتي للعملاء.
قناة مدير الملعب تطبيق مدير الملعب
الموظفون في الموقع ينشئون حجوزات من تطبيق مدير الملعب على الجوال. مديرو الملاعب محدودون بالملاعب المُعيّنة لهم فقط — لا يمكنهم رؤية أو حجز ملاعب أخرى.
قناة العميل 2.5%، المستهدف 15%+
العملاء المصادقون يحجزون ذاتياً عبر تطبيق الجوال. حالياً أصغر قناة، لكن تنميتها أولوية أعمال قصوى. تستخدم الحجز المؤقت للفترات لمنع التعارضات أثناء عملية الحجز.
قناة العقود 6.8%
إنشاء تلقائي من الاتفاقيات المتكررة. حجوزات العقود لديها أقل معدل إلغاء بنسبة 12.7%، مما يجعلها أكثر مصدر إيرادات موثوقية.
إدارة الحجز المؤقت
عندما يبدأ العميل نموذج الحجز في تطبيق الجوال، يُوضع حجز مؤقت على الفترة المختارة لمنع عميل آخر من حجز نفس الوقت بينما يكمل العميل الأول النموذج.
- مدة الحجز المؤقت: 5 دقائق من الإنشاء
- مهمة التنظيف: مهمة خلفية تعمل كل 30 ثانية لإنهاء الحجوزات المؤقتة المنتهية
- حجز مؤقت واحد لكل فترة: مُطبّق بقيد
UNIQUE(field_id, date, start_time)على جدولbooking_holds - المشكلة المحلولة: في v2، كان العملاء يملأون نموذج الحجز بالكامل ثم يكتشفون أن الفترة محجوزة عند الإرسال. هذا سبّب معدل مغادرة 50.1% لصفحة الحجز. الحجز المؤقت يزيل مشكلة "اكتشاف عدم التوفر متأخراً".
منع التعارض
UNIQUE على (field_id, date, start_time) مع معاملة serializable أو trigger من نوع INSTEAD OF INSERT يتحقق من عدم وجود نطاقات زمنية متداخلة. SQL Server سيرفض عمليات الإدراج المتعارضة. حتى لو كان هناك خطأ في كود التطبيق، فإن قيد قاعدة البيانات يمنع الحجز المزدوج.
في v2، كان منع التعارض يُعالج بالكامل في كود التطبيق. مولّد العقود تجاوز هذه الفحوصات، مما أدى إلى 4 حوادث تلف بيانات حيث شغل حجزان نفس الملعب والفترة الزمنية. في v3، قاعدة البيانات نفسها تُطبّق هذا الثابت — لا يمكن لأي مسار في كود التطبيق انتهاكه.
الانتقالات التلقائية (المهام الخلفية)
المهام الخلفية تتولى تقدم الحالة التلقائي حتى لا تعلق الحجوزات في حالات قديمة.
| الانتقال | المحفّز | الغرض |
|---|---|---|
| confirmed → completed | بعد 24 ساعة من وقت انتهاء الحدث | يُعلّم الجلسات تلقائياً كمكتملة بعد حدوثها |
| booked → cancelled | بعد 24 ساعة من تاريخ الجلسة إذا لم يُؤكد | يُلغي تلقائياً الحجوزات التي لم تُؤكد أبداً (بدون دفع، بدون إجراء من المسؤول) |
الحقول الإلزامية في كل حجز
| الحقل | النوع | الوصف |
|---|---|---|
field_id |
string (max 15 chars) | أي ملعب الحجز له |
customer_id |
string (max 15 chars) | من حجز (العميل) |
creator_id |
string (max 15 chars) | من أنشأ الحجز (قد يختلف عن العميل — مثلاً، المسؤول يحجز نيابة عن العميل) |
date |
DATE | تاريخ الجلسة |
start_time |
TIME | وقت بداية الفترة |
end_time |
TIME | وقت نهاية الفترة |
status |
string (enum stored as string) | الحالة الحالية: booked, confirmed, completed, cancelled, no_show |
source |
string (enum stored as string) | القناة: admin, fm, customer, contract, guest |
price |
DECIMAL, NOT NULL, > 0 | تسعير إلزامي — في v2 كان 58.3% من الحجوزات بسعر صفر. قيد قاعدة البيانات يمنع هذا. |
payment_status |
string (enum stored as string) | حالة الدفع: unpaid, partial, paid, refunded |
الإلغاء
الإلغاء عملية محكومة مع توثيق إلزامي لدعم تحليل الأعمال وتقليل معدلات الإلغاء.
- مسموح من: حالة booked أو confirmed
- رمز سبب إلزامي: كل إلغاء يتطلب سبباً (لماذا تم الإلغاء؟). هذا ليس اختيارياً.
- سجل التدقيق: الإلغاء ينشئ سجل تدقيق غير قابل للتغيير يوثّق من ألغى، ومتى، ولماذا
- لا يمكن إلغاء المكتمل أو عدم الحضور: بمجرد وصول الحجز إلى حالة نهائية، لا يمكن إلغاؤه