إنتقل إلى المحتوى الرئيسي

مخطط قاعدة البيانات

نظرة عامة

تستخدم منصة شام باص قاعدة بيانات PostgreSQL عبر Supabase مع Row Level Security (RLS) لضمان عزل البيانات. يتغير عدد الجداول والكيانات مع الترحيلات، لذلك يجب اعتبار هذا المستند خريطة مفاهيمية وعملية، بينما يبقى مجلد الترحيلات هو المرجع النهائي للأسماء والبنية الدقيقة.

ملاحظات توافق العقود

تحافظ الترحيلات الحديثة على أعمدة توافقية بجانب الحقول الأساسية حتى لا تنكسر واجهات PostgREST أو تطبيقات الموبايل أثناء تطور المخطط. أمثلة مهمة:

  • cities.name يزامن من name_ar حتى تعمل الاستعلامات القديمة التي تطلب cities.name.
  • companies.name وحقول حالة الاشتراك/التحقق تكمل الحقول العربية/الإنجليزية المستخدمة في لوحات الإدارة والشركات.
  • routes.base_price يزامن مع price_base.
  • trips.price يزامن مع price_base، مع أعمدة توقيت التشغيل started_at و completed_at.
  • bookings.is_checked_in, checked_in_at, checked_in_by, و checkin_method تعكس سجلات passenger_checkins.
  • booking_qr_tokens.is_valid, used_at, و used_by تعكس حالة المسح.
  • دوال مثل get_booking_by_code و token_blocklist موجودة كعقود حية لاختبارات الويب والموبايل.

Journey Booking aggregate

Migration 00227 separates the customer purchase from its operational segments:

journeys
└─ journey_bookings
├─ booking_passengers
├─ bookings (one compatibility segment per journey_leg)
│ ├─ booking_extras
│ └─ journey_seat_assignments
└─ journey_tickets (one Passenger × Journey Leg)

journey_booking_sessions is the opaque checkout capability. journey_resource_hold_batches groups all trip_resource_holds for one quote so exact seats and non-seat resources either reserve together or all roll back. Direct client mutation is revoked; quote, hold, release, confirmation, expiry, and ticket verification use caller-bound SECURITY DEFINER commands with RLS-protected reads.

Migration 00291 adds service-only confirmation bridges that temporarily project the API-verified Traveler UUID into the atomic database command. A malformed or expired Bearer token is rejected by the HTTP boundary and cannot silently become an anonymous checkout. Migration 00292 gives every compatibility Booking the same durable journey_booking_sessions owner projection as a native Journey, including a backfill and lifecycle trigger. A confirmed legacy Booking can therefore be reopened from My Trips without turning its public URL into an authorization credential.

Migrations 00293 and 00294 keep isolated demo discovery deterministic. New demo profiles are published before insertion, while get_demo_passenger_launch_option returns only a future Stop Call pair backed by an active vehicle and open Seat inventory on the Asia/Damascus service date. The function is executable only by service_role; public visibility remains governed by the existing exact-demo identity scope.

Migrations 00295 and 00296 repair runtime lineage and security rather than changing the Journey contract. They reassert the final 0029200294 definitions before normalizing only three known release-candidate checksums, enable RLS and least-privilege grants on migration/auth/quality metadata, pin every legacy SECURITY DEFINER search path, and revoke stale client access to the service-only PII backfill checkpoint. pnpm audit:db-schema verifies the running database separately from the migration source inventory.

The compatibility trips.available_seats counter changes only when a Booking Segment becomes active. Temporary holds remain in trip_resource_inventory.held_quantity; confirmed seats and Extras move to reserved_quantity. The reconciliation function derives both values from canonical rows and rejects any capacity shrink below held plus reserved demand.

journey_orders groups one or two Journey Bookings so outbound and return travel share one idempotent checkout lifecycle. Confirmation is atomic across every Journey, segment, passenger, assignment, Extra, and Ticket. The database command rejects stale/foreign holds and rolls the whole order back if any leg cannot be confirmed.

Migration 00305 adds Company-controlled confirmation without turning a reservation into a usable Ticket prematurely:

company_booking_confirmation_policies
└─ Company mode + review window + applicable traveler channels

journey_booking
├─ confirmation_status
├─ reservation_submitted_at / confirmation_deadline
└─ journey_booking_confirmation_requests (one immutable policy snapshot per operator)

AUTO_CONFIRM remains the onboarding/backfill default. Under MANUAL_CONFIRM, checkout consumes the authoritative resource hold but moves the aggregate to PENDING_CONFIRMATION; Ticket rows use PENDING_CONFIRMATION with an empty credential. decide_company_booking_confirmation is service-role-only, verifies the authenticated Company actor and tenant, is idempotent, and approves the whole Journey/Order only after every required request closes successfully. Rejection or deadline expiry atomically cancels the affected scope and releases inventory. Direct document issuance and direct Ticket promotion are guarded by the same aggregate state.

journey_booking_boarding_authorized is the shared fulfillment predicate. It requires confirmation_status=CONFIRMED, exactly party_size × leg_count Tickets, and a current signed credential plus boarding authorization timestamp for every Ticket. journey_order_boarding_authorized requires every item to pass. Customer Web/mobile, Driver manifests, QR/document endpoints, and Company workspaces consume these projections rather than independently inferring eligibility. A confirmed connected Journey cannot be cancelled through the legacy single-segment RPC; the Company must use the aggregate-aware Trip operations/disruption workflow.

Migration 00306 closes the older direct-Booking compatibility seam. After the canonical projection trigger creates a Journey aggregate, an ordered non-callable trigger initializes the same Company policy and request graph. It also suppresses the obsolete segment-level confirmed notification when a manual decision request exists. The no-policy checkout bridges, boarding predicates, and traveler-history projection helpers are explicitly service-only; no anonymous or authenticated role can invoke them directly.

Migration 00307 prevents a never-issued Ticket from gaining a credential when operator rejection or timeout changes it from PENDING_CONFIRMATION to a terminal state. Previously issued cancellations keep their signed audit state; never-authorized cancellations remain unsigned. The validated journey_tickets_boardable_has_credential constraint independently guarantees that every boardable Ticket has a non-empty signed payload and authorization timestamp.

Migration 00263 makes a later bookings.trip_id change a canonical Journey operation rather than a disconnected compatibility update. The database locks the Journey, validates the replacement Trip's operator and endpoints, moves the Journey Leg, seat assignments, Tickets, Extras, and pending reminders, reconciles old and new inventory, updates aggregate totals, and replaces the accepted quote with a new price snapshot. It rejects active holds, finalized attendance, cross-operator moves, incompatible stops, timing conflicts, and insufficient resource capacity. A Booking Segment and its Journey Leg therefore cannot point at different Trips after a successful transaction.

Each journey_ticket is the boarding identity for exactly one Passenger and one Journey Leg. Migration 00247 records attendance in journey_ticket_checkins, whose ticket_id is unique. validate_and_checkin_journey_ticket verifies the assigned Driver, live assignment, Trip match, credential version, HMAC signature, Ticket/Booking state, offline timestamp, and optional coordinates while holding the relevant rows. Replays return the existing per-Ticket result and cannot check in another Passenger in the same Booking. Clients exchange only the versioned Ticket UUID/signature credential; names, phones, seats, and Booking references are not embedded in the QR payload.

Connected operations and tenant isolation

The operational graph uses the same Trip truth consumed by booking and settlement:

Trip
├─ Trip Stop Calls
├─ Trip Assignments → Driver / Bus
├─ Trip Operation Events
├─ Trip Disruptions
├─ Passenger Announcements
├─ Bus Locations
├─ Bookings / Check-ins
└─ Partner Settlement Line

Telemetry and check-in mutations are available only through caller-bound commands. Direct driver writes to live-location storage are revoked. Migrations 0023700242 consolidate access through narrow SECURITY DEFINER predicates that resolve the authenticated Platform role, Company membership, assigned Driver, or owning Traveler before RLS returns operational or financial records. No policy may infer authorization from client-supplied identity fields.

The exception snapshot ranks active critical alerts, disruptions, delay, and unhealthy GPS before future assignment gaps. The result reports both the complete count inside the requested window and the bounded number of rows returned, so the control-center UI cannot mistake pagination for operational truth.

Demo reset and expiry use the private dependency-ordered teardown introduced by 00250. It rejects non-demo Companies and any cross-tenant Journey or Journey Order before enabling a transaction-local teardown flag. Immutable finance/settlement documents and Trip-history guards honor that flag only after independently verifying companies.is_demo = true; ordinary tenant history remains immutable. Migration 00303 applies the same boundary to one-use document-transfer capabilities: a verified demo document removes its events, recipient session, and grant in dependency order while the grant's Company proof is still available. Outside that transaction, document-share events remain append-only and both direct mutation and ordinary cascading deletion fail closed.

Document-share issuer resolution also returns the canonical database role. Migration 00304 keeps Company finance documents (INVOICE, PAYMENT_RECEIPT) within OWNER/ADMIN, leaves operational confirmations available to booking roles, and excludes platform SUPPORT from document issuance. The client cannot submit or override this role.

:::caution Confirmed legacy ACL gap

The historical development schema still contains broad grants on some pre-foundation tables. They are not covered by the new narrow operational predicates. A compatibility-aware caller audit and explicit grant normalization are required before those legacy ACLs can be called production-hardened; global revocation without that audit could break existing Supabase clients.

:::

مخطط العلاقات (ER Diagram)

erDiagram
companies ||--o{ routes : "has"
companies ||--o{ buses : "owns"
companies ||--o{ company_users : "employs"
companies ||--o{ drivers : "has"

routes ||--o{ trips : "schedules"
routes ||--o{ trip_schedules : "recurs_on"
routes }o--|| cities : "origin"
routes }o--|| cities : "destination"

buses ||--o{ trips : "assigned_to"
buses ||--o{ trip_schedules : "recurring_assignment"
buses ||--|| bus_layouts : "has_layout"

trips ||--o{ bookings : "has"
trips ||--o{ seats : "has"
trips ||--o{ trip_assignments : "assigned"
trip_schedules ||--o{ trips : "generates"

companies ||--o{ company_cash_shifts : "reconciles"
company_cash_shifts ||--o{ company_cash_adjustments : "records"
companies ||--o{ notifications : "owns"
notifications ||--o{ notification_deliveries : "fans_out"
notification_deliveries ||--o{ notification_delivery_attempts : "attempts"
bookings ||--o{ financial_documents : "snapshots"
financial_documents ||--o{ document_render_events : "renders"
bookings ||--o{ booking_documents : "snapshots"
booking_documents ||--o{ document_render_events : "renders"
bookings ||--o{ document_share_grants : "shares one issued document"
booking_documents o|--o{ document_share_grants : "shared copy"
financial_documents o|--o{ document_share_grants : "shared copy"
document_share_grants ||--o| document_share_sessions : "accepted once"
document_share_grants ||--o{ document_share_events : "audits"

passengers ||--o{ bookings : "makes"
companies ||--|| company_loyalty_programs : "owns"
company_loyalty_programs ||--o{ loyalty_tiers : "defines"
company_loyalty_programs ||--o{ loyalty_rewards : "offers"
passengers ||--o{ loyalty_accounts : "owns_per_company"
loyalty_tiers ||--o{ loyalty_accounts : "classifies"
loyalty_accounts ||--o{ loyalty_transactions : "records"
loyalty_accounts ||--o{ loyalty_redemptions : "creates"
loyalty_rewards ||--o{ loyalty_redemptions : "redeemed_as"
bookings ||--o{ booking_qr_tokens : "has"
bookings ||--o{ passenger_checkins : "checked_in"
loyalty_redemptions o|--o| bookings : "applied_to"

drivers ||--o{ trip_assignments : "drives"
drivers ||--o{ bus_locations : "reports"

notifications ومراقبة التسليم

يمثل notifications حدثاً منطقياً واحداً مع tenant واللغة والـcriticality ومعرفات correlation/causation. تنشئ كل قناة خارجية صفاً مستقلاً في notification_deliveries مع recipient masked/hash وidempotency key وقفل worker وعداد retry. تسجل notification_delivery_attempts كل محاولة، بينما تجعل notification_delivery_events callbacks المزودين idempotent. لا تكتب تطبيقات الويب حالات التسليم مباشرة؛ تستخدم RPCs في الترحيل 00217 للإنشاء والclaim والتكملة والfan-out.

financial_documents وdocument_render_events

financial_documents سجل immutable لإيصال دفع أو إشعار استرداد. يحفظ رقم المستند وtenant والحجز ونوع المصدر ومعرفه وsnapshot وSHA-256 ونسخة القالب ووقت الإصدار. trigger يمنع UPDATE وDELETE، وissue_financial_document وحده يحل المصدر المصرح إلى snapshot idempotent.

document_render_events سجل append-only لكل PDF مولد، بما فيه التذاكر والتأكيدات والتقارير التي لا تحتاج financial snapshot. يحفظ نوع المستند ومفتاحه واللغة والنسخة وchecksum الملف والحجم وrenderer trace والمستخدم. لا يعيد API ملفاً مالياً إذا فشل إدراج حدث التصيير.

booking_documents ومساحة عمل الحجز

booking_documents سجل immutable لتأكيد الحجز والفاتورة وتأكيد الإلغاء. يحفظ رقم مستند متسلسلاً، نوعه، مراجعته، snapshot كامل aggregate، SHA-256، نسخة القالب، المُصدر، وعلاقة supersession بالمراجعة السابقة. تمنع triggers التحديث والحذف، وتبقى القراءة المباشرة محجوبة؛ الإصدار والقراءة يمران عبر حدود service-role بعد تحقق tenant والدور في API.

يمثل build_company_booking_snapshot الإسقاط الثابت، بينما يجمع get_company_booking_workspace الحقيقة الحية لصفحة الشركة مع المسافرين والتذاكر والمقاعد والحضور/عدم الحضور والدفعات والمستندات والاتصالات. أضيف booking_document_id إلى document_render_events لربط كل ملف بالمراجعة التي صُيّرت فعلياً.

document_share_grants, document_share_sessions, وdocument_share_events

تمثل هذه الجداول تسليم نسخة مستند واحدة بين جهازين ولا تمثل صلاحية حجز أو صعود:

  • يربط document_share_grants حجزاً بمستند Booking أو Financial immutable واحد فقط. المنحة single-use، قابلة للإلغاء، وتخزن hash السر لا السر الخام.
  • ينشئ القبول الوحيد صفاً في document_share_sessions مع hash جديد وجلسة حتى 30 دقيقة تنتهي كذلك بعد 15 دقيقة خمول. هوية المنحة والجلسة immutable.
  • تستقبل RPCs مدد TTL ضمن حدود ثابتة وتحسب كل expires_at باستخدام clock_timestamp() داخل PostgreSQL؛ لا تعد ساعة حاوية الويب مصدراً موثوقاً لانتهاء الصلاحية.
  • يسجل document_share_events أحداث CREATED/PREVIEWED/ACCEPTED/VIEWED/ DOWNLOADED/REVOKED append-only. لا يسجل التسليم إلا بعد تصيير PDF ناجح، ويضم checksum وحجم الملف وrenderer trace.

كل الجداول تستخدم RLS forced ولا تملك أدوار anon أو authenticated أو حتى service_role وصولاً مباشراً. تعمل الدوال المحددة والمقفلة فقط من service role بعد أن يحل API هوية المسافر أو السائق المعيّن أو مستخدم الشركة أو مسؤول المنصة. لا تقبل الدوال actor/company scope يختاره العميل.


الجداول الأساسية

companies - شركات النقل

جدول شركات النقل المسجلة على المنصة.

العمودالنوعالوصف
idUUIDالمعرف الفريد
name_arTEXTالاسم بالعربية
name_enTEXTالاسم بالإنجليزية
phoneTEXTرقم الهاتف
whatsappTEXTرقم الواتساب
emailTEXTالبريد الإلكتروني
logo_urlTEXTرابط الشعار
descriptionTEXTالوصف
is_verifiedBOOLEANتم التحقق
is_activeBOOLEANنشط
is_suspendedBOOLEANموقوف
suspended_atTIMESTAMPTZتاريخ الإيقاف
suspension_reasonTEXTسبب الإيقاف
warning_countINTEGERعدد التحذيرات
is_demoBOOLEANtenant تجريبي مؤقت
is_internalBOOLEANtenant QA إنتاجي خاص
demo_expires_atTIMESTAMPTZوقت انتهاء العرض
demo_slugTEXTمعرف العرض الفريد
demo_configJSONBالهوية والمسارات المخصصة
created_atTIMESTAMPTZتاريخ الإنشاء

عندما تكون is_demo=true يجب أن يكون demo_expires_at في المستقبل حتى تسمح طبقات API وRLS بالوصول. أما is_internal=true فيحصر الشركة بحسابات QA المعلّمة في app_metadata. يمنع قيد قاعدة البيانات جمع المجالين على شركة واحدة، وتستبعد العروض العامة كليهما. يتطلب اكتشاف Trip الموجه للمسافر أيضاً أن تكون الشركة نشطة وغير موقوفة وأن تكون profile_status=PUBLISHED؛ يحتفظ service_role فقط بمسار التشغيل الموثوق للمهام الخلفية. يصنف الترحيل 00289 بيانات QA الإنتاجية القديمة ذات التوقيع الصريح INTERNAL PRODUCTION QA ACCOUNT ويمنح النطاق الداخلي فقط لهويات تملك علاقة شركة أو سائق أو حجز موثوقة بذلك الـtenant.


demo_access_grants - منح الوصول للعروض

العمودالنوعالوصف
idUUIDمعرف المنحة
company_idUUIDtenant التجريبي
token_hashTEXTبصمة SHA-256 للرمز؛ لا يخزن الرمز الخام
prospect_nameTEXTاسم جهة الاتصال الاختياري
prospect_emailTEXTبريد جهة الاتصال الاختياري
expires_atTIMESTAMPTZوقت انتهاء المنحة
revoked_atTIMESTAMPTZوقت الإلغاء الفوري
last_used_atTIMESTAMPTZآخر وصول ناجح
use_countINTEGERعدد مرات الاستخدام

demo_personas - شخصيات العرض

يربط الشخصيات الثلاث (owner, passenger, driver) بهويات auth.users وسجلات الشركة/المسافر/السائق. يستخدمه خادم bootstrap فقط ولا يقرأه العميل مباشرة.

demo_outbox - صندوق الرسائل المحاكى

يسجل البريد وSMS وواتساب والتنبيهات التي كان سيولدها العرض دون إرسالها إلى مزود خارجي. يظهر آخر محتوى للعميل داخل مركز العرض.


cities - المدن السورية

جدول المدن السورية (21 مدينة محافظة).

العمودالنوعالوصف
idUUIDالمعرف الفريد
name_arTEXTالاسم بالعربية
name_enTEXTالاسم بالإنجليزية
governorateTEXTالمحافظة
latitudeDOUBLE PRECISIONخط العرض
longitudeDOUBLE PRECISIONخط الطول
is_activeBOOLEANنشط

routes - المسارات

جدول المسارات بين المدن.

العمودالنوعالوصف
idUUIDالمعرف الفريد
company_idUUIDمعرف الشركة
origin_city_idUUIDمدينة المغادرة
dest_city_idUUIDمدينة الوصول
distance_kmINTEGERالمسافة بالكيلومتر
duration_minutesINTEGERالمدة بالدقائق
price_baseINTEGERالسعر الأساسي للمسار
is_activeBOOLEANنشط

القيود: فريد (company_id, origin_city_id, dest_city_id)

تقرأ لوحة الشركة المسارات عبر get_dashboard_routes_overview(company_id, is_active, route_id). تجمع الدالة trips_count التاريخي وscheduled_trips_count القادم داخل PostgreSQL بدلاً من تحميل معرفات كل الرحلات إلى خادم Next.js. تتحقق الدالة من عضوية auth.uid() النشطة ودور OWNER أو ADMIN أو DISPATCHER في الشركة المطلوبة، ولا تمنح التنفيذ لـ anon. يدعم الاستعلام الفهرس المركب trips(company_id, route_id).


buses - الباصات

جدول أسطول الباصات.

العمودالنوعالوصف
idUUIDالمعرف الفريد
company_idUUIDمعرف الشركة
plate_numberTEXTرقم اللوحة
capacityINTEGERالسعة
bus_typebus_typeالنوع (STANDARD, VIP, SLEEPER)
amenitiesTEXT[]المرافق
is_activeBOOLEANنشط

تقرأ لوحة الشركة الحالة التشغيلية عبر get_dashboard_fleet_operations(company_id, bus_id). تعيد الدالة صفاً واحداً فقط لكل باص لديه رحلة بحالة BOARDING أو DEPARTED، وتختار أحدث مغادرة عند وجود بيانات تاريخية متعارضة. يمنع ذلك استنتاج حالة الأسطول من أول 1,000 رحلة يعيدها PostgREST، ويبقي حجم الاستجابة متناسباً مع عدد الباصات لا مع تاريخ الرحلات. الدالة SECURITY DEFINER لكنها تتحقق صراحة من صلاحية tenant ومن عضوية OWNER أو ADMIN أو DISPATCHER النشطة، ولا تمنح التنفيذ لـ anon.


trips - الرحلات

جدول الرحلات المجدولة.

العمودالنوعالوصف
idUUIDالمعرف الفريد
company_idUUIDمعرف الشركة، يزامن من المسار
route_idUUIDمعرف المسار
bus_idUUIDمعرف الباص
pickup_stop_idUUIDنقطة الصعود المملوكة/العامة المختارة
pickup_name_ar/enTEXTاسم نقطة الصعود المثبت على الرحلة
pickup_address_ar/enTEXTعنوان نقطة الصعود المثبت
pickup_latitudeNUMERICخط العرض المثبت
pickup_longitudeNUMERICخط الطول المثبت
pickup_directions_ar/enTEXTإرشادات الوصول المثبتة
pickup_location_confirmedBOOLEANأكد المشغل النقطة لهذه الرحلة
departure_timeTIMESTAMPTZوقت المغادرة
arrival_timeTIMESTAMPTZوقت الوصول
price_baseINTEGERالسعر الأساسي (ل.س)
price_vipINTEGERسعر VIP
available_seatsINTEGERالمقاعد المتاحة
total_seatsINTEGERإجمالي المقاعد
statustrip_statusالحالة
schedule_idUUIDالسلسلة المتكررة، إن وجدت
boarding_started_atTIMESTAMPTZبدء الصعود الفعلي
actual_departure_timeTIMESTAMPTZالانطلاق الفعلي
actual_arrival_timeTIMESTAMPTZالوصول الفعلي
delay_minutesINTEGERمدة التأخير
delay_reasonTEXTسبب التأخير
cancellation_reasonTEXTسبب الإلغاء
closed_atTIMESTAMPTZإغلاق السجل التشغيلي
status_changed_byUUIDمستخدم الشركة المنفذ

حالات الرحلة:

  • SCHEDULED - مجدولة
  • BOARDING - الصعود جاري
  • DEPARTED - غادرت
  • ARRIVED - وصلت
  • CANCELLED - ملغاة
  • DELAYED - متأخرة
  • IN_PROGRESS - قيمة توافق لرحلة قيد التنفيذ
  • IN_TRANSIT - قيمة توافق لرحلة على الطريق
  • COMPLETED - مغلقة تشغيلياً

تقرأ لوحة الشركة القائمة عبر get_dashboard_trips_page(company_id, page, per_page, status, date, route_id, search_route_ids, search_bus_ids). تعيد الدالة الصفوف والعدد الإجمالي وعدادات الحالات في استدعاء واحد، بحد أقصى 100 صف للصفحة، وتتحقق من عضوية auth.uid() النشطة في نفس الشركة قبل أي قراءة. عند تمرير date تستخدم حدود يوم دمشق وترتيباً زمنياً تصاعدياً؛ ومن دونه تعيد السجل الكامل من الأحدث إلى الأقدم. تشمل النتيجة السائق والتأخير والأوقات الفعلية.

منذ migration 00251 تحوّل الدالة تاريخ يوم دمشق إلى حدّي UTC ثم تقارن departure_time مباشرةً من دون cast على العمود. تُرشّح الرحلات وتحسب العدادات وتختار الصفحة أولاً، ثم تربط المسار والمدن والباص والسائق لصفوف الصفحة فقط. تدعم ذلك الفهارس idx_trips_company_departure_id و idx_trips_company_status_departure_id حتى لا تمسح صفحة تشغيل صغيرة سجل الشركة كاملاً أو تنفذ joins لعشرات آلاف الرحلات قبل LIMIT.

بعد إنشاء الرحلة يشغّل PostgreSQL initialize_trip_seats() لإنشاء صفوف المقاعد القديمة المتوافقة. تنفذ الدالة إدراجاً واحداً عبر generate_series بدلاً من حلقة لكل مقعد، وهي SECURITY DEFINER مع search_path ثابت حتى لا تعيد سياسات RLS وتقاطعات tenant لكل مقعد ولا تتجاوز مهلة طلب المستخدم الموثق.

يمثل trips.driver_id هوية السائق التشغيلية في company_users.id، بينما يمثل trip_assignments.driver_id ملف السائق في drivers.id الذي يستهلكه تطبيق السائق. تربط drivers.company_user_id الهويتين. يشغّل sync_trip_driver_assignment() بعد إنشاء الرحلة أو تغيير السائق أو الباص، ويحدّث تكليف تطبيق السائق ذرياً. يمنع تغيير السائق أو الباص بعد بدء التشغيل، وتحجب الجاهزية الانطلاق إذا كان الملف أو التكليف غير مرتبط أو غير متطابق.

يملك trigger validate_trip_pickup_location() نقطة الصعود: يثبت أنها في مدينة منشأ المسار وأن النقطة عامة أو مملوكة للشركة، وينسخ الاسم والعنوان والإحداثيات والإرشادات إلى صف الرحلة. يمنع تغييرها بعد وجود حجز. ينشئ sync_trip_stop_calls_from_route() أول trip_stop_call من نقطة الصعود الدقيقة، وتحمل trip_schedules.pickup_stop_id النقطة إلى الرحلات المتكررة. تدعم فهارس الشركة/النقطة/المغادرة استعلامات التشغيل من دون N+1.

تعرض public_trips لقنوات العملاء كامل snapshot الرحلة مع استبعاد شركات الديمو. وبما أن PostgreSQL يثبت أعمدة SELECT * عند إنشاء الـ view، يعيد migration 00286 بناء هذا الإسقاط بعد إضافة حقول نقطة الصعود؛ يمنع contract قاعدة البيانات رجوع حالة تظهر فيها النقطة في trips وتختفي من البحث أو تفاصيل الرحلة العامة.

trip_schedules - سلاسل الرحلات

يحفظ جدول trip_schedules تعريف السلسلة بعد أن تنشئ فعلياً موعداً صالحاً واحداً على الأقل. يتضمن الشركة والمسار والباص والسائق الاختياري ونطاق التواريخ وأيام الأسبوع والوقت والمدة والأسعار والملاحظات وهوية المنشئ. يرتبط كل صف منشأ في trips.schedule_id بالسلسلة، مع قيد فريد على (schedule_id, departure_time).

تنفذ الدالة plan_company_trip_series(...) المعاينة والإنشاء. تتحقق من دور OWNER أو ADMIN أو DISPATCHER، ومن ملكية المسار والباص والسائق، وصلاحية مخطط المقاعد، ومن نطاق لا يتجاوز 180 يوماً. تفحص تداخلات الباص والسائق وصيانة الباص ورخصة السائق. عند commit=true تستخدم أقفالاً advisory للباص والسائق وتعيد الفحص داخل المعاملة قبل الإدراج، لذلك لا تعتمد الكتابة على معاينة قديمة.

trip_operation_events وتشغيل الرحلة

يحفظ trip_operation_events تغيير الحالة والحضور وعدم الحضور والتراجع، مع الشركة والرحلة والحجز الاختياري والحالة السابقة والجديدة والسبب والبيانات الإضافية ومنفذ الإجراء والوقت. الجدول للقراءة فقط أمام أدوار الشركة المسموحة؛ كل كتابة تتم من دوال التشغيل الموثقة.

تعيد get_company_trip_dispatch_v2(company_id, trip_id) لقطة الغرفة: الرحلة ونقطة الصعود المثبتة والباص والسائق والكشف والعدادات والعوائق والإجراءات والأحداث. تفحص get_trip_dispatch_blockers(...) مخطط المقاعد والصيانة والسائق والرخصة والحجوزات غير المحسومة.

تنفذ transition_company_trip(...) دورة SCHEDULED/DELAYED → BOARDING → DEPARTED → ARRIVED → COMPLETED تحت قفل صف، وتسمح بالإلغاء قبل الانطلاق فقط مع سبب. تزامن الدالة trip_assignments والأوقات وتكتب حدثاً واحداً. عند الإلغاء يلغي trigger الحجوزات النشطة ويضيف إشعارات requires_refund_review=true من دون تزوير حقيقة الدفع أو تنفيذ استرداد ضمني.

تستخدم اللقطة هوية الحضور الأساسية نفسها التي يستخدمها تطبيق السائق: تذكرة Journey Ticket واحدة لكل مسافر ومقطع رحلة. ترجع manifest_item_id و manifest_item_type=JOURNEY_TICKET لكل مسافر، مع fallback مؤقت من نوع BOOKING للحجوزات القديمة التي لم تنشئ تذاكر Journey بعد.

تنفذ set_company_trip_manifest_attendance(...) الحضور وعدم الحضور والتراجع أثناء BOARDING فقط. عند NO_SHOW تنشئ سجلاً مستقلاً للمسافر مع السبب وهوية الموظف؛ وعند RESTORE تعكس ذلك السجل بدلاً من حذفه. تقفل الدالة التذكرة والحجز وتزامن الحضور والحالة الإجمالية والتكليف وسجل التشغيل في معاملة واحدة. لا يؤدي غياب مسافر واحد في حجز جماعي إلى تغيير بقية المسافرين. تستخدم دوال SECURITY DEFINER تحققاً صريحاً من auth.uid() والشركة والدور، ولا يملك anon حق التنفيذ.

تنفذ confirm_company_booking(company_id, booking_id) تأكيد الحجز المعلق تحت قفل الحجز والرحلة وبعد التحقق من عضوية الشركة وحالة الرحلة وموعدها. يمنع trigger الحالة انتقالات التأكيد والحضور والإكمال المباشرة؛ وتفتحها أعلام معاملة محلية داخل دوال SECURITY DEFINER الموثقة فقط، مع استثناء service_role للأحداث النظامية الموثوقة مثل webhooks الدفع.

لقطة تحليلات الشركة

تقرأ صفحة التحليلات الدالة get_dashboard_analytics_snapshot(company_id). تجمع الدالة الإيرادات والحجوزات والرحلات والإشغال وسلاسل 30 يوماً و12 شهراً وأداء المسارات وتكرار السفر في استدعاء JSON واحد. تعتمد على trips.company_id وbookings.company_id المفهرسين بدلاً من بناء مرشحات trip_id=in.(...) قد تتجاوز حد سطر الطلب في Kong أو حد 1,000 صف في PostgREST. تعمل الدالة بصلاحية SECURITY DEFINER مع search_path ثابت، لكنها ترفض أي مستخدم ليس عضواً نشطاً بدور OWNER أو ADMIN في الشركة المطلوبة، ولا تمنح التنفيذ لـ anon.

يقرأ تبويب التحليلات المتقدمة الدالة get_dashboard_advanced_analytics(company_id). تجمع الدالة الإلغاءات لآخر 30 يوماً، نشاط العملاء والاحتفاظ، تاريخ 60 يوماً للتوقع، ترتيب المسارات للفترة الحالية والسابقة، ووقت الحجز المسبق. يحسب إشغال المسار من سعة الرحلات الفريدة ومقاعد الحجوزات المؤكدة أو المسجلة أو المكتملة، وليس من تكرار سعة الباص لكل حجز. تخضع الدالة لحراس العضوية والدور والصلاحيات نفسها ولا يمكن لـ anon تنفيذها.


passengers - الركاب

جدول بيانات الركاب.

العمودالنوعالوصف
idUUIDالمعرف الفريد
auth_user_idUUIDمعرف مستخدم Supabase
phoneTEXTرقم الهاتف (فريد)
nameTEXTالاسم
emailTEXTالبريد الإلكتروني
email_verifiedBOOLEANتم التحقق من البريد
email_verified_atTIMESTAMPTZوقت التحقق من البريد
whatsappTEXTرقم الواتساب
reminder_enabledBOOLEANتفعيل تذكيرات الرحلات
reminder_channelTEXTقناة التذكير: whatsapp أو sms أو both
reminder_day_beforeBOOLEANإرسال تذكير قبل الرحلة بيوم
reminder_hour_beforeBOOLEANإرسال تذكير قبل الرحلة بساعة
last_bookingTIMESTAMPTZآخر حجز
demo_company_idUUIDtenant العرض عند كون المسافر تجريبياً

bookings - الحجوزات

جدول حجوزات التذاكر.

العمودالنوعالوصف
idUUIDالمعرف الفريد
company_idUUIDالشركة المنسوخة من الرحلة لعزل الاستعلام
trip_idUUIDمعرف الرحلة
passenger_idUUIDمعرف الراكب
seat_numberTEXTأسماء المقاعد القانونية مفصولة بفواصل
seat_numbersINTEGER[]إسقاط توافق للمقاعد الرقمية فقط
seat_typeseat_typeنوع المقعد
seats_countINTEGERعدد المقاعد
priceINTEGERالسعر القديم المتوافق
total_priceINTEGERإجمالي سعر الحجز (ل.س)
subtotal_priceNUMERIC(12,2)السعر قبل خصم الولاء
discount_amountNUMERIC(12,2)قيمة خصم الولاء المطبقة
loyalty_redemption_idUUIDقسيمة الولاء المستخدمة، إن وجدت
passenger_nameTEXTاسم منسوخ للبحث والتوافق
passenger_phoneTEXTهاتف منسوخ للبحث والتوافق
statusbooking_statusالحالة
booking_codeTEXTكود الحجز (SB-XXXXXX)
whatsapp_sentBOOLEANتم إرسال الواتساب
notesTEXTملاحظات
confirmed_atTIMESTAMPTZتاريخ التأكيد
cancelled_atTIMESTAMPTZتاريخ الإلغاء

يمكن أن يحتوي seat_number قيماً مثل 1D. يبقى seat_numbers INTEGER[] للتوافق مع الحجوزات الرقمية القديمة فقط، ويكون NULL عند وجود أسماء حرفية. تتحقق عقود الحجز من الاسم مقابل bus_layouts.layout_document وتكتب الإسقاطين داخل المعاملة نفسها.

يحفظ الحجز الذي استخدم مكافأة السعر قبل الخصم وقيمة الخصم والسعر النهائي. يفرض القيد أن subtotal_price = total_price + discount_amount، ويمنع الفهرس الجزئي ربط قسيمة ولاء بأكثر من حجز. في الذهاب والعودة يرتبط سجل القسيمة بحجز الذهاب وتوزع قيمة الخصم على السجلين مع الاحتفاظ بتفصيل كل سعر.

حالات الحجز:

  • PENDING - قيد الانتظار
  • CONFIRMED - مؤكد
  • CANCELLED - ملغي
  • COMPLETED - مكتمل
  • NO_SHOW - لم يحضر

تقرأ قائمة لوحة الشركة عبر get_dashboard_bookings_page(company_id, page, per_page, status, search, trip_date, trip_id, created_from, created_to). تتحقق الدالة من auth.uid() وعضوية الشركة النشطة والدور التشغيلي، وتعيد صفوف الصفحة والعدد الإجمالي وعدادات الحالات والإيرادات من نطاق مادي واحد. يفرض مسار القائمة حداً أقصى 100 صف، بينما تسمح الدالة بـ 5,000 صف فقط لمسار التصدير المحمي. تستخدم الفهارس (company_id, created_at DESC, id DESC) و (company_id, status, created_at DESC, id DESC) لاستقرار الترتيب والصفحات.

يعتمد تتبع الراكب العام على get_public_trip_tracking(booking_code) بدلاً من منح anon قراءة الجداول. يعيد العقد JSON محدوداً بالرحلة والمدن والشركة وحالة التكليف وآخر 100 موضع ضمن نافذة التشغيل. كود الحجز هو capability، لذلك لا يدخل في السجلات ولا تعيد الدالة أي PII أو بيانات دفع أو مقعد أو ملاحظات أو معرّفات السائق.


جداول الولاء والمكافآت

company_loyalty_programs

صف واحد لكل شركة يحدد سياسة الكسب والاستبدال والإحالة والانتهاء والشروط. يبدأ متوقفاً افتراضياً. تملك شركة النقل الاقتصاد التجاري للبرنامج؛ لا يملك Admin المنصة حق تعديل الأرصدة أو المكافآت.

loyalty_tiers وloyalty_accounts

يعرّف loyalty_tiers مستويات كل برنامج وأسماءها العربية والإنجليزية وحد النقاط والمزايا ومعامل الكسب. يربط loyalty_accounts حساباً واحداً بكل زوج (company_id, passenger_id) ويحفظ رصيداً مستقلاً لكل شركة. يمنع قيد فريد إنشاء حسابين للمسافر نفسه داخل الشركة، ولا توجد عملية نقل بين الشركات.

loyalty_transactions

سجل append-only لكل شركة وحساب ولكل كسب أو استبدال أو انتهاء أو تعديل. يحفظ مقدار النقاط موجباً أو سالباً، وbalance_after، ومرجع الحجز أو الرحلة، والوصف باللغتين، ووقت انتهاء النقاط المكتسبة عند وجوده. يمنع idempotency_key تكرار مكافأة الحدث نفسه.

loyalty_rewards

كتالوج مكافآت الشركة. يحتوي نوع المكافأة والنقاط المطلوبة ونسبة أو قيمة الخصم والحد الأدنى للحجز والمسارات والأيام المؤهلة وحدود الاستبدال ونافذة الصلاحية. يدعم المخطط أنواعاً إضافية، لكن تدفق حجز العميل يطبق discount وfree_trip فقط.

loyalty_redemptions

يمثل القسيمة الناتجة من إنفاق النقاط. يحفظ رمز RWD-… الفريد والحالة والانتهاء والحجز المستخدم وقيمة الخصم، إضافة إلى reward_snapshot حتى لا يؤدي تعديل المكافأة لاحقاً إلى تغيير شروط قسيمة سبق للعميل شراؤها.

تنفذ redeem_company_loyalty_reward الخصم وإنشاء الحركة والقسيمة وتحديث عداد المكافأة مع أقفال صفوف تمنع الإنفاق المزدوج وتتحقق من تطابق الشركة. تتحقق preview_customer_loyalty_redemption من الأهلية ومن أن شركة القسيمة تشغل الرحلة من دون الاستهلاك. تنفذ apply_company_loyalty_referral إنشاء الإحالة ومكافأة الترحيب في معاملة واحدة وتمنع الإحالة الذاتية والتكرار والتداخل بين الشركات. تنفذ دالتا create_customer_booking_atomic وcreate_customer_round_trip_booking_atomic حجز المقاعد واستهلاك القسيمة مرة واحدة داخل المعاملة نفسها. كل هذه الدوال الخادمة مقيدة بدور service_role.

تحتفظ الصفوف التي أنشأها النظام المركزي القديم بعلامة is_legacy_platform=true لأغراض التاريخ فقط. لا تُسند قسراً إلى شركة ولا تظهر كرصيد شركة لأن ملكيتها التجارية لا يمكن استنتاجها بأمان.


جداول المقاعد والتخطيط

seats - مقاعد الرحلات

سجلات المقاعد الفردية لكل رحلة.

العمودالنوعالوصف
idUUIDالمعرف الفريد
trip_idUUIDمعرف الرحلة
booking_idUUIDمعرف الحجز
passenger_idUUIDمعرف الراكب
seat_numberINTEGERرقم المقعد
statusTEXTالحالة (AVAILABLE, BOOKED, LOCKED, RESERVED)
seat_typeTEXTالنوع (STANDARD, VIP, WINDOW, AISLE)
price_modifierDECIMALمعامل السعر

bus_layouts - تخطيطات الباصات

تكوين شبكة تخطيط الباص. للميزات الجديدة يعتبر layout_document هو مصدر الحقيقة الوحيد القابل للكتابة، بينما تبقى الأعمدة الشبكية القديمة وbus_layout_positions إسقاطات توافقية مشتقة منه لدعم الشاشات والبيانات القديمة.

العمودالنوعالوصف
idUUIDالمعرف الفريد
bus_idUUIDمعرف الباص
layout_documentJSONBالتخطيط القانوني بإحداثيات x/y/width/height/rotation/deck
layout_schema_versionINTEGERإصدار مخطط JSON
statusTEXTdraft / active / archived
validation_statusTEXTunvalidated / valid / invalid
validation_errorsJSONBأخطاء التحقق عند الحفظ
parent_layout_idUUIDالإصدار السابق عند التفرع/الأرشفة
activated_atTIMESTAMPTZوقت تفعيل التخطيط
archived_atTIMESTAMPTZوقت أرشفة التخطيط
grid_rowsINTEGERإسقاط توافق من layout_document
grid_colsINTEGERإسقاط توافق من layout_document
aisle_after_colINTEGER[]إسقاط توافق للممرات القديمة
driver_positionJSONBإسقاط توافق لموقع السائق
door_positionsJSONB[]إسقاط توافق لمواقع الأبواب
toilet_positionJSONBإسقاط توافق لموقع دورة المياه
total_seatsINTEGERإجمالي المقاعد المحسوب من التخطيط
versionINTEGERرقم الإصدار
is_activeBOOLEANنشط

الحفظ يتم عبر RPC save_bus_layout_document(p_bus_id, p_layout_document, p_save_mode) حتى يتم حفظ التخطيط، تحديث bus.capacity، وتجديد bus_layout_positions في معاملة واحدة. لا تستخدم واجهات جديدة إدخالاً مباشراً إلى bus_layout_positions.


bus_layout_positions - مواقع المقاعد

مواقع المقاعد الفردية داخل التخطيط. هذا الجدول إسقاط توافق مشتق من layout_document وليس مصدر كتابة للتخطيطات الجديدة.

العمودالنوعالوصف
idUUIDالمعرف الفريد
layout_idUUIDمعرف التخطيط
seat_idTEXTالمعرف الثابت للمقعد داخل layout_document
seat_numberVARCHAR(10)رقم المقعد
row_indexINTEGERفهرس الصف
col_indexINTEGERفهرس العمود
seat_type_idUUIDنوع المقعد
row_labelVARCHAR(5)تسمية الصف (A, B, ...)
is_windowBOOLEANمقعد نافذة
is_aisleBOOLEANمقعد ممر
rotationINTEGERالدوران (0, 90, 180, 270)

seat_type_definitions - تعريفات أنواع المقاعد

أنواع المقاعد المخصصة لكل شركة.

العمودالنوعالوصف
idUUIDالمعرف الفريد
company_idUUIDمعرف الشركة
codeVARCHAR(50)الرمز (vip, economy, wheelchair)
name_arVARCHAR(100)الاسم بالعربية
name_enVARCHAR(100)الاسم بالإنجليزية
colorVARCHAR(7)اللون (HEX)
iconVARCHAR(50)الأيقونة
price_modifierDECIMALمعامل السعر (1.0 = الأساسي)
is_accessibleBOOLEANمتاح لذوي الاحتياجات
priority_boardingBOOLEANصعود أولوي
extra_legroomBOOLEANمساحة إضافية للأرجل

bus_templates - قوالب الباصات

قوالب تخطيط مسبقة للإعداد السريع.

العمودالنوعالوصف
idUUIDالمعرف الفريد
name_arVARCHAR(100)الاسم بالعربية
name_enVARCHAR(100)الاسم بالإنجليزية
capacityINTEGERالسعة
grid_rowsINTEGERعدد الصفوف
grid_colsINTEGERعدد الأعمدة
has_toiletBOOLEANيحتوي دورة مياه
seat_positionsJSONBمواقع المقاعد

القوالب المتوفرة:

  1. باص عادي 40 مقعد (10×4)
  2. باص 50 مقعد مع دورة مياه (13×4)
  3. باص VIP 30 مقعد (10×3)
  4. ميني باص 20 مقعد (5×4)
  5. باص كبير 55 مقعد مع دورة مياه (14×4)

جداول المستخدمين

admin_users - مشرفو المنصة

العمودالنوعالوصف
idUUIDالمعرف الفريد
auth_user_idUUIDمعرف auth.users
emailTEXTالبريد الإلكتروني
nameTEXTالاسم
roleadmin_roleالدور (SUPER_ADMIN, ADMIN, SUPPORT)
is_activeBOOLEANنشط
last_login_atTIMESTAMPTZآخر تسجيل دخول
totp_enabledBOOLEANهل المصادقة الثنائية مفعلة
totp_secretTEXTغلاف AES-256-GCM إصداري totp:v1 لسر TOTP
backup_codesTEXT[]رموز الاسترداد المخزنة

company_users - مستخدمو الشركات

العمودالنوعالوصف
idUUIDالمعرف الفريد
auth_user_idUUIDمعرف auth.users
company_idUUIDمعرف الشركة
emailTEXTالبريد الإلكتروني
nameTEXTالاسم
rolecompany_roleالدور
is_activeBOOLEANنشط

أدوار الشركة:

  • OWNER - مالك
  • ADMIN - مدير
  • DISPATCHER - منسق
  • STAFF - موظف

verified_second_factor_sessions - جلسات العامل الثاني

قائمة سماح خادمة فقط تربط جلسة Supabase التي اجتازت TOTP بهوية المشرف أو السائق. لا تتاح مباشرةً لدوري anon أو authenticated.

العمودالنوعالوصف
subject_typeTEXTadmin أو driver
subject_idUUIDمعرف المشرف أو السائق
auth_user_idUUIDهوية Supabase التي اجتازت التحقق
session_idUUIDمعرف جلسة JWT المحددة
verified_atTIMESTAMPTZوقت نجاح العامل الثاني
last_seen_atTIMESTAMPTZآخر استخدام محمي
expires_atTIMESTAMPTZانتهاء الثقة حتى لو بقيت جلسة Auth صالحة
revoked_atTIMESTAMPTZوقت الإلغاء المبكر
client_ipINETعنوان العميل عند التحقق
user_agentTEXTوكيل المستخدم عند التحقق

drivers - السائقون

العمودالنوعالوصف
idUUIDالمعرف الفريد
company_user_idUUIDهوية السائق التشغيلية في company_users
company_idUUIDمعرف الشركة
license_numberTEXTرقم الرخصة
license_expiryDATEتاريخ انتهاء الرخصة
phoneTEXTرقم الهاتف
fcm_tokenTEXTرمز Firebase
current_trip_idUUIDالرحلة الحالية
last_latitudeDECIMALآخر خط عرض
last_longitudeDECIMALآخر خط طول
total_tripsINTEGERإجمالي الرحلات
on_time_percentageDECIMALنسبة الالتزام بالمواعيد

جداول تتبع السائق

trip_assignments - تعيينات الرحلات

العمودالنوعالوصف
idUUIDالمعرف الفريد
trip_idUUIDمعرف الرحلة
driver_idUUIDمعرف ملف السائق في drivers
bus_idUUIDمعرف الباص
statustrip_assignment_statusالحالة
started_atTIMESTAMPTZوقت البدء
completed_atTIMESTAMPTZوقت الانتهاء
total_checked_inINTEGERإجمالي الصعود
total_no_showsINTEGERإجمالي الغياب
total_collectedDECIMALالمبلغ المحصل

يوجد تكليف واحد لكل رحلة. ينشأ ويُزامن من trips.driver_id عبر drivers.company_user_id؛ لذلك لا تكتب واجهات لوحة الشركة معرف ملف drivers مباشرة في trips.driver_id.

لا يملك السائق UPDATE مباشراً على التكليف. تسجل mark_driver_assignment_en_route(...) استعداده للطريق قبل دخول الرحلة غرفة التشغيل، ثم تستخدم انتقالات الرحلة الموثقة التسلسل الأمامي فقط: BOARDING → DEPARTED → ARRIVED → COMPLETED. يتحقق كل انتقال من هوية السائق ومن تكليفه الفعلي ويزامن الرحلة والتكليف والأوقات وسجل التشغيل في معاملة واحدة؛ ولا يستطيع دور السائق تأخير الرحلة أو إلغاءها أو إرجاعها إلى حالة سابقة.


bus_locations - مواقع الباصات

العمودالنوعالوصف
idUUIDالمعرف الفريد
trip_idUUIDمعرف الرحلة
driver_idUUIDمعرف السائق
latitudeDECIMALخط العرض
longitudeDECIMALخط الطول
speedDECIMALالسرعة
headingDECIMALالاتجاه
recorded_atTIMESTAMPTZوقت التسجيل
is_offline_recordBOOLEANسجل غير متصل

passenger_checkins - تسجيل صعود الركاب

العمودالنوعالوصف
idUUIDالمعرف الفريد
booking_idUUIDمعرف الحجز
trip_idUUIDمعرف الرحلة
driver_idUUIDمعرف السائق
checked_in_atTIMESTAMPTZوقت الصعود
check_in_methodcheckin_methodطريقة التسجيل
is_no_showBOOLEANلم يحضر

طرق التسجيل:

  • QR_SCAN - مسح QR
  • MANUAL - يدوي
  • AUTO - تلقائي

booking_qr_tokens - رموز QR للحجوزات

العمودالنوعالوصف
idUUIDالمعرف الفريد
booking_idUUIDمعرف الحجز
trip_idUUIDمعرف الرحلة
tokenTEXTالرمز المميز
qr_dataJSONBبيانات QR
expires_atTIMESTAMPTZتاريخ الانتهاء
scanned_atTIMESTAMPTZوقت المسح
scanned_byUUIDمسح بواسطة

توفر الدالة get_trip_qr_tokens(trip_id, driver_id) نسخة HMAC مختصرة للتخزين عند السائق. التنفيذ مقصور على دور authenticated، ويربط معرف السائق بـ auth.uid() قبل قراءة أي اسم أو هاتف لراكب. يبقى bookings.seat_number نصياً للتوافق، وتعيد الدالة إسقاطاً عددياً آمناً لتطبيق السائق.

تحفظ trip_messages رسائل الرحلة. يقرأ السائق ويرسل فقط لرحلة مكلف بها بعد حل هويته عبر is_own_driver، ويقرأ الراكب فقط محادثة رحلة يملك لها حجزاً. تسمح امتيازات المتصفح بـ SELECT، INSERT، وتعديل عمود is_read فقط؛ لا ينطبق RLS على TRUNCATE لذلك هذا الامتياز مسحوب صراحةً من anon وauthenticated.

يحفظ trip_payments التحصيل الذي ينفذه السائق. يفرض الفهرس الجزئي الفريد uq_trip_payments_booking_id سجلاً واحداً لكل booking_id. الكتابة من دور authenticated متاحة فقط عبر collect_trip_payment(...): تربط الدالة p_driver_id بـ auth.uid()، وتقفل التكليف والحجز، وتتحقق من الرحلة والمبلغ المتبقي وحالة التشغيل، ثم تحدّث trip_payments وbookings.is_paid/payment_status/paid_amount/paid_at/payment_collected وtrip_assignments.total_collected ذرياً. إعادة الطلب idempotent، بينما الجدول نفسه للقراءة فقط أمام المستخدم الموثق ومحجوب بالكامل عن anon. بقيت الدالة القديمة increment_assignment_collected لدور service_role فقط.

تعيد الدالة get_dashboard_payment_ledger(...) إسقاطاً موحداً لعمليات payment_transactions وتحصيل trip_payments وحقيقة الحجز النقدية، مع إحصاءات وpagination وفلاتر الخادم. يطبع trigger normalize_booking_payment_truth() حقول payment_status, is_paid, payment_collected, paid_amount, وpaid_at حتى لا تكتب قنوات التحصيل حقائق متناقضة.

تنفذ دوال verify_company_payment, refund_company_payment, cancel_company_payment, وupdate_company_payment_notes قرارات الشركة تحت قفل العملية والحجز مع تحقق صريح من عضوية OWNER أو ADMIN. الاسترداد كامل فقط، والإلغاء مقصور على الحالات غير المكتملة، وملاحظات العملية النهائية غير قابلة للتعديل. تبقى الدالة التاريخية apply_provider_payment_webhook محجوبة خلف service_role ولا يستدعيها أي endpoint نشط. يمنع الترحيل 00278 جلسات anon وauthenticated من كتابة مزود غير CASH إلى سجلات العميل أو السائق، كما يقيد إعدادات كل شركة على النقد فقط إلى أن يعتمد مزود رسمي.

سُحبت امتيازات INSERT, UPDATE, وDELETE على payment_transactions وعلى bookings من anon وauthenticated. لذلك لا تكفي سياسة RLS لفتح كتابة مباشرة؛ كل تغيير حجز أو قرار مالي يمر عبر دالة المجال المناسبة التي تملك قفلها وتحققها وسجلها في المعاملة نفسها.

company_cash_shifts وcompany_cash_adjustments

يحفظ company_cash_shifts دورة صندوق الشركة من رصيد البداية إلى الإغلاق، بإجمالي التحصيل والحركات والمبلغ المتوقع والمعلن والفارق وهوية الفاتح والمغلق. يسمح فهرس جزئي بصندوق مفتوح واحد فقط لكل شركة. يحفظ company_cash_adjustments كل مصروف أو استرداد أو تصحيح أو حركة أخرى مع مبلغ غير صفري وملاحظة وهوية المنفذ. القراءة ودوال open, adjust, وclose مقصورة على مالك الشركة أو مديرها النشط؛ وتستخدم عملية الإغلاق قفل صف وتحسب التحصيل من وقت فتح الوردية حتى لحظة الإغلاق.


جداول الدعم الفني

support_tickets - تذاكر الدعم

العمودالنوعالوصف
idUUIDالمعرف الفريد
ticket_numberTEXTرقم التذكرة
passenger_idUUIDمعرف الراكب
company_idUUIDمعرف الشركة
booking_idUUIDمعرف الحجز
typeticket_typeالنوع
subjectTEXTالموضوع
descriptionTEXTالوصف
statusticket_statusالحالة
priorityticket_priorityالأولوية
assigned_toUUIDمسند إلى
resolved_atTIMESTAMPTZتاريخ الحل

أنواع التذاكر:

  • COMPLAINT - شكوى
  • REFUND_REQUEST - طلب استرداد
  • BOOKING_ISSUE - مشكلة حجز
  • GENERAL_INQUIRY - استفسار عام
  • SUGGESTION - اقتراح

أولويات التذاكر:

  • LOW - منخفضة
  • MEDIUM - متوسطة
  • HIGH - عالية
  • URGENT - عاجلة

ticket_messages - رسائل التذاكر

العمودالنوعالوصف
idUUIDالمعرف الفريد
ticket_idUUIDمعرف التذكرة
sender_typemessage_sender_typeنوع المرسل
sender_idUUIDمعرف المرسل
sender_nameTEXTاسم المرسل
contentTEXTالمحتوى
is_internalBOOLEANرسالة داخلية
attachmentsTEXT[]المرفقات

جداول الإدارة

admin_permissions - صلاحيات المشرفين

العمودالنوعالوصف
idUUIDالمعرف الفريد
codeTEXTالرمز
name_arTEXTالاسم بالعربية
name_enTEXTالاسم بالإنجليزية
categoryTEXTالفئة

admin_audit_log - سجل المراجعة

العمودالنوعالوصف
idUUIDالمعرف الفريد
admin_user_idUUIDمعرف المشرف
admin_nameTEXTاسم المشرف
admin_emailTEXTبريد المشرف
actionTEXTالإجراء
resource_typeTEXTنوع المورد
resource_idUUIDمعرف المورد
detailsJSONBالتفاصيل
ip_addressINETعنوان IP
created_atTIMESTAMPTZالتاريخ

audit_log - سجل التدقيق العام

سجل ملحق فقط (append-only) للأحداث غير الإدارية، مثل نشاط الركاب والسائقين والشركات وأحداث النظام. لا يسمح بالوصول المباشر من anon أو authenticated؛ تتم الكتابة والقراءة من خدمات الخادم باستخدام service_role.

العمودالنوعالوصف
idUUIDالمعرف الفريد
actor_idTEXTمعرف الجهة المنفذة
actor_typeTEXTراكب/شركة/مشرف/سائق/نظام
actor_emailTEXTالبريد عند توفره
company_idUUIDنطاق الشركة عند توفره
actionTEXTالإجراء
resource_typeTEXTنوع المورد
resource_idTEXTمعرف المورد
detailsJSONBتفاصيل منظمة
ip_addressTEXTعنوان IP الموثق أو وسم المصدر القديم
user_agentTEXTوكيل المستخدم
created_atTIMESTAMPTZوقت التسجيل

تحتفظ أعمدة التوافق table_name, record_id, old_data, new_data, وuser_id بعقد مزامنة تسجيل الحضور دون اتصال حتى تُرحّل الدالة القديمة إلى العقد الموحد.


waitlist - قائمة انتظار الشركات

العمودالنوعالوصف
idUUIDالمعرف الفريد
emailTEXTبريد الشركة أو الشخص المهتم
company_nameTEXTاسم الشركة الاختياري
phoneTEXTرقم الهاتف الاختياري
sourceTEXTمصدر التسجيل
statusTEXTpending/contacted/converted/rejected
notesTEXTملاحظات داخلية
created_atTIMESTAMPTZوقت التسجيل
contacted_atTIMESTAMPTZوقت التواصل
contacted_byUUIDالمشرف الذي تواصل مع الطلب

جداول الإشعارات

notifications - إشعارات الحجوزات

العمودالنوعالوصف
idUUIDالمعرف الفريد
booking_idUUIDمعرف الحجز
typenotification_typeالنوع
channelnotification_channelالقناة (SMS, WHATSAPP)
statusnotification_statusالحالة
sent_atTIMESTAMPTZوقت الإرسال
error_messageTEXTرسالة الخطأ

driver_notifications - إشعارات السائقين

العمودالنوعالوصف
idUUIDالمعرف الفريد
driver_idUUIDمعرف السائق
titleTEXTالعنوان
bodyTEXTالمحتوى
typedriver_notification_typeالنوع
dataJSONBالبيانات
read_atTIMESTAMPTZوقت القراءة

push_subscriptions - اشتراكات الإشعارات

العمودالنوعالوصف
idUUIDالمعرف الفريد
passenger_idUUIDمالك راكب، عند اشتراك العميل
company_user_idUUIDمالك شركة؛ تستخدمه أيضاً اشتراكات السائق المرتبط
admin_user_idUUIDمالك مشرف المنصة
device_idVARCHARمعرف جهاز اختياري
device_typeVARCHARweb أو ios أو android
device_nameVARCHARوصف الجهاز بحد 255 محرفاً
push_tokenTEXTهدف التسليم الفريد والمعتمد للـ upsert
endpointTEXTعنوان Web Push عند استخدام VAPID
p256dh_keyTEXTمفتاح تشفير Web Push
auth_keyTEXTسر مصادقة Web Push
fcm_tokenTEXTرمز FCM الاختياري
ntfy_topicVARCHARموضوع ntfy لتطبيقي Flutter
is_activeBOOLEANحالة الاشتراك
last_used_atTIMESTAMPTZآخر تسجيل أو استخدام
expires_atTIMESTAMPTZانتهاء اختياري

يفرض المخطط وجود مالك واحد فقط من أعمدة المالك الثلاثة، ويفرض تفرد push_token. لا تستخدم الأعمدة التاريخية user_id, user_type, p256dh, auth, أو device_info. عند تسجيل السائق تُحل هويته الموثقة أولاً ثم يُحفظ الاشتراك تحت drivers.company_user_id. موضوع ntfy مرتبط بهوية الجلسة (shambus-customer-{authUserId} أو shambus-driver-{driverId}) ولا يقبل موضوعاً اختيارياً من العميل.


جداول التقييم والإحصائيات

trip_reviews - مراجعات العملاء المنشورة

يربط الجدول كل مراجعة بحجز وراكب ورحلة وشركة، ويسمح بمراجعة واحدة لكل حجز. يقبل تقييماً عاماً من 1 إلى 5 وتقييمات اختيارية للراحة والالتزام والنظافة والسائق، مع تعليق وحالة إشراف PUBLISHED, HIDDEN, أو FLAGGED.

لا ينشئ العميل مراجعة إلا لحجز يملكه بعد اكتمال الرحلة، ولا يستطيع بعد ذلك تغيير معرف الحجز أو الراكب أو الرحلة أو الشركة أو حالة الإشراف. يتحقق مسار التعديل مجدداً من ملكية الحجز واكتمال الرحلة، ويقرأ موظف الشركة مراجعات شركته فقط عندما يكون حسابه نشطاً. يعيد الترحيل 00216 تثبيت هذه السياسات وtrigger ثبات الهوية قبل تطبيع checksum التاريخي المعروف للترحيل 00203.

trip_feedback - تقييمات الرحلات

العمودالنوعالوصف
idUUIDالمعرف الفريد
booking_idUUIDمعرف الحجز
trip_idUUIDمعرف الرحلة
passenger_idUUIDمعرف الراكب
overall_ratingINTEGERالتقييم العام (1-5)
driver_ratingINTEGERتقييم السائق
bus_condition_ratingINTEGERتقييم حالة الباص
punctuality_ratingINTEGERتقييم الالتزام بالمواعيد
commentTEXTالتعليق
is_anonymousBOOLEANمجهول الهوية

company_monthly_stats - إحصائيات الشركات الشهرية

العمودالنوعالوصف
idUUIDالمعرف الفريد
company_idUUIDمعرف الشركة
yearINTEGERالسنة
monthINTEGERالشهر
trips_countINTEGERعدد الرحلات
bookings_countINTEGERعدد الحجوزات
revenueINTEGERالإيرادات
avg_ratingDECIMALمتوسط التقييم

أنواع البيانات المخصصة (Enums)

-- أنواع الباصات
CREATE TYPE bus_type AS ENUM ('STANDARD', 'VIP', 'SLEEPER');

-- حالات الرحلات
CREATE TYPE trip_status AS ENUM ('SCHEDULED', 'BOARDING', 'DEPARTED',
'ARRIVED', 'CANCELLED', 'DELAYED');

-- حالات الحجوزات
CREATE TYPE booking_status AS ENUM ('PENDING', 'CONFIRMED', 'CANCELLED',
'COMPLETED', 'NO_SHOW');

-- أدوار المشرفين
CREATE TYPE admin_role AS ENUM ('SUPER_ADMIN', 'ADMIN', 'SUPPORT');

-- أدوار الشركة
CREATE TYPE company_role AS ENUM ('OWNER', 'ADMIN', 'DISPATCHER', 'STAFF');

-- أنواع الإشعارات
CREATE TYPE notification_type AS ENUM ('BOOKING_CONFIRMED', 'BOOKING_CANCELLED',
'TRIP_REMINDER', 'TRIP_DEPARTED',
'TRIP_CANCELLED');

-- قنوات الإشعارات
CREATE TYPE notification_channel AS ENUM ('WHATSAPP', 'SMS');

-- أنواع تذاكر الدعم
CREATE TYPE ticket_type AS ENUM ('COMPLAINT', 'REFUND_REQUEST',
'BOOKING_ISSUE', 'GENERAL_INQUIRY',
'SUGGESTION');

-- حالات التعيينات
CREATE TYPE trip_assignment_status AS ENUM ('ASSIGNED', 'EN_ROUTE',
'BOARDING', 'IN_PROGRESS',
'COMPLETED', 'CANCELLED');

الفهارس الأساسية

-- فهارس الأداء
CREATE INDEX idx_trips_departure ON trips(departure_time);
CREATE INDEX idx_trips_status ON trips(status);
CREATE INDEX idx_bookings_status ON bookings(status);
CREATE INDEX idx_bookings_code ON bookings(booking_code);
CREATE INDEX idx_seats_trip_status ON seats(trip_id, status);
CREATE INDEX idx_bus_locations_trip ON bus_locations(trip_id);
CREATE INDEX idx_passengers_phone ON passengers(phone);

ملاحظات مهمة

  1. جميع المعرفات UUID: نستخدم UUID بدلاً من التسلسل الرقمي للأمان وقابلية التوزيع.

  2. الطوابع الزمنية: جميع التواريخ بتوقيت UTC (TIMESTAMPTZ).

  3. الأسعار بالليرة السورية: الأسعار مخزنة كـ INTEGER بدون فواصل عشرية.

  4. Soft Delete: لا نحذف البيانات نهائياً، نستخدم is_active = false.

  5. التدقيق: جميع العمليات الحساسة مسجلة في admin_audit_log.