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

واجهات برمجة بوابة العملاء

Base URL: https://shambus.com/api | http://localhost:3000/api

نطاق الصفحة

هذه الصفحة مرجع route families العالي الأولوية لبوابة العملاء. المصادقة موثقة بعمق أعلى من بقية العائلات.

المصادقة

إرسال رمز OTP

POST /auth/customer/send-otp

الجسم:

{
"phone": "+963912345678",
"method": "sms"
}

لتيليجرام يمكن إرسال telegramChatId يدوياً. إذا شغّل المستخدم بوت شام باص وشارك رقم الهاتف، يستطيع الخادم حل Chat ID تلقائياً من الرقم:

{
"phone": "+963912345678",
"method": "telegram",
"telegramChatId": "123456789"
}

الاستجابة الأساسية:

{
"success": true,
"message": "تم إرسال رمز التحقق عبر رسالة نصية SMS",
"fallbackToSms": false,
"actualChannel": "sms"
}

ملاحظات:

  • الحقل الحالي المعتمد هو method وليس channel.
  • القنوات المعروضة حالياً: sms و telegram.
  • sms هو الافتراضي ويستخدم مزود Verify مضبوطاً في OTP_PROVIDER.
  • تقبل الواجهة الخلفية whatsapp مؤقتاً من العملاء القدامى، لكنها تحول الطلب إلى SMS بدون أي اتصال بـ Meta.
  • telegram يتطلب TELEGRAM_BOT_TOKEN على الخادم وخدمة telegram-bot لربط رقم الهاتف مع Chat ID، أو إرسال telegramChatId يدوياً.
  • يجب اختبار إرسال حي إلى رقم سوري قبل تفعيل أي مزود في الإنتاج.
  • في بيئات التطوير فقط قد تظهر حقول إضافية مثل devMode أو otpCode إذا فُعل ذلك صراحة.

التحقق من OTP

POST /auth/customer/verify-otp

الجسم:

{
"phone": "+963912345678",
"code": "123456",
"method": "telegram",
"name": "أحمد محمد"
}

الاستجابة الأساسية:

{
"success": true,
"data": {
"user": {
"id": "uuid",
"phone": "+963912345678",
"name": "أحمد محمد",
"email": "ahmed@example.com"
},
"isNewUser": false,
"emailVerificationPending": false
}
}

ملاحظات:

  • المسار ينشئ جلسة ويضبط cookies آمنة من نوع HttpOnly.
  • لا تعتمد على access token داخل body فقط عند بناء عميل حديث.

مبادلة هوية Google الأصلية

POST /auth/customer/google/mobile
Content-Type: application/json
{
"idToken": "<native-google-id-token>"
}

يتحقق الخادم من توقيع الرمز ومصدره وجمهوره وانتهائه ومن أن البريد موثق؛ لا يقبل اسماً أو بريداً أو Google subject مستقلاً من الواجهة. بعد منع تعارض حسابات الموظفين، يربط الهوية بالمسافر القانوني ويصدر جلسة Supabase عادية تتضمن accessToken وrefreshToken. تعيد الاستجابة أيضاً isNewUser وneedsPhone وبيانات المسافر الآمنة. المسار محدود حسب IP وبصمة Google subject ولا يحتاج CSRF لأنه لا يعتمد على cookies الواردة. يرفض المسار أي طلب يحمل Origin أو Sec-Fetch-Site؛ يجب على المتصفح استخدام عقد web أو pwa المرتبط بـnonce، ولا يمكنه استعمال عقد الموبايل لتجاوز حماية المتصفح.

لا يغير هذا التدفق كلمة مرور موجودة. إذا كان المسافر مسجلاً بالهاتف فقط، يضاف بريد Google الموثق إلى auth user نفسه قبل إصدار الجلسة؛ أي تعارض يفشل مغلقاً ولا ينشئ حساباً موازياً.

دخول Google على الويب

GET /auth/customer/google/nonce
POST /auth/customer/google/web

يعيد nonce معرّف Web client العام وflowId وnonce خاماً، بينما يحتفظ الخادم ببصمة nonce فقط في cookie قصيرة العمر من نوع HttpOnly. يعرض العميل زر Google الرسمي ويرسل idToken وflowId وreturnTo النسبي إلى web. يتحقق الخادم من توقيع الرمز وnonce والأصل وحد الطلبات ثم يضبط access/refresh/CSRF cookies من نوع HttpOnly؛ لا يعيد الرموز في JSON ولا يقبل بيانات ملف Google كحقول مستقلة.

يرجع الحقل data.next إلى الموقع القانوني المحفوظ. إذا كانت العملية التالية تتطلب هاتفاً ولم يكن موجوداً، يشير إلى /auth/complete-phone مع الاحتفاظ بمسار الحجز الكامل. إنشاء nonce منفصل لكل flowId يجعل علامات التبويب المتوازية آمنة.

دخول Google من Flutter Web/PWA

GET /auth/customer/google/nonce
OPTIONS /auth/customer/google/pwa
POST /auth/customer/google/pwa
Origin: https://app.shambus.com

يستخدم PWA bootstrap وnonce نفسيهما، لكنه يرسل idToken وflowId إلى مسار محدود صراحة بأصل app.shambus.com (أو localhost:8180 في التطوير). يتحقق الخادم من Origin وSec-Fetch-Site وnonce ومن الرمز ثم يعيد جلسة bearer اللازمة لعميل Flutter المشفر محلياً. لا يقبل المسار cookies من أصول عشوائية، ولا يوسع CORS لبقية API، ويستهلك nonce بعد محاولة ناجحة واحدة.

ربط Google بحساب مسافر قائم

POST /profile/security/identities/google
X-CSRF-Token: <customer-csrf-cookie>

يستخدم العقد نفسه idToken وflowId ولكنه يتطلب جلسة مسافر وCSRF. ينفذ RPC ذرياً إثبات ملكية Passenger ومنع تعارض subject أو البريد ومنع حساب الموظف. تبقى إمكانية الفصل مشروطة بوجود طريقة دخول أخرى صالحة.

استكمال رقم الهاتف بعد Google

POST /auth/customer/complete-phone
Authorization: Bearer <supabase-access-token>
{
"phone": "+963944123456",
"code": "123456",
"method": "sms"
}

يتحقق المسار من تحدي OTP أولاً، ثم ينفذ complete_traveler_phone_identity ذرياً. إذا كان الرقم يعود إلى سجل مسافر قديم آمن الدمج، تعيد الاستجابة passenger_id القانوني الجديد كي يحدّث تطبيق الهاتف جلسته المشفرة. وجود سجلين تشغيليين غير قابلين للدمج يفشل مغلقاً بالرمز ACCOUNT_LINK_REQUIRES_RECOVERY ولا ينقل الحجوزات اعتماداً على بيانات الواجهة.

تحديث رمز الوصول

POST /auth/customer/refresh

السلوك الحالي:

  • يبحث أولاً عن refresh token داخل cookie آمنة
  • ما زال يقبل refreshToken في body كتوافق خلفي

الاستجابة:

{
"success": true,
"data": {
"token": "new_jwt_token",
"refreshToken": "new_refresh_token",
"expiresAt": 1713176400
}
}

الحصول على بيانات المستخدم

GET /auth/customer/me

الاستجابة:

عند عدم وجود جلسة زائر، يرجع المسار استجابة ناجحة بقيمة data: null حتى لا تنتج الصفحات العامة أخطاء مصادقة في المتصفح. إذا وُجدت جلسة أو ترويسة تفويض غير صالحة، يرجع 401.

{
"data": {
"id": "uuid",
"phone": "+963912345678",
"name": "أحمد محمد",
"email": "ahmed@example.com",
"gender": "",
"nationalId": "",
"created_at": "2024-01-01T00:00:00Z"
}
}

تسجيل الخروج

POST /auth/customer/logout

يقوم هذا المسار بإنهاء الجلسة الحالية ومسح الكوكيز ذات الصلة.

الرحلات والحجوزات

العائلةعمق التوثيق
المصادقةnear-spec
الرحلات والمساراتreference
الحجوزات والتذاكرreference
الدعم والمدفوعات والولاءreference

تنبيه: تم التحقق من قسم المصادقة في هذه الصفحة مقابل route handlers الحالية. أما الرحلات والحجوزات وبقية المسارات فتُعامل هنا كمرجع عملي عالي المستوى ويجب مراجعتها مع كل تغيير عقدي مؤثر.

دليل مواقع السفر

GET /journey-locations

يعيد دليلاً عاماً متدرجاً يميز بين اختيار المدينة كاملة واختيار نقطة صعود محددة. البنية هي cities[] وlocations[]، ويحمل كل عنصر في locations مفتاحاً مستقراً مثل CITY:<uuid> أو STOP:<uuid>. تشمل نقاط التوقف العامة محطات الباص والمواقف والمطارات ونقاط الصعود، ولا يعرض المسار المستودعات DEPOT. ينفذ الخادم قراءتين مفهرستين متوازيتين بدلاً من استعلام لكل مدينة، ويخزن الجواب العام مؤقتاً لمدة ساعة. تمر قراءة anon عبر predicates خاصة بدل إعطاء الزائر أي صلاحية على جداول هوية المشرفين أو موظفي الشركات.

تستخدم صفحة الهبوط وصفحة البحث وتطبيق العميل الدليل نفسه. عند نشر متدرج يفشل فيه المسار الجديد، يستطيع تطبيق العميل الرجوع مؤقتاً إلى /cities كبحث على مستوى المدينة فقط؛ لا يحول معرف محطة إلى معرف مدينة. تحفظ عمليات البحث الحديثة نوع الموقع ومعرفه كي لا تتسع محطة محددة بصمت إلى المدينة كاملة.

المسارات الشائعة

GET /routes/popular

تستخدم هذه القراءة مجال الرحلات نفسه المستخدم في البحث والحجز: الزائر والحساب العادي يريان إحصاءات شركات الإنتاج الحقيقية فقط، والحساب الداخلي يرى إحصاءات QA الداخلية فقط، وشخصية العرض ترى شركة العرض المطابقة فقط. تنفذ قاعدة البيانات التجميع بعد تطبيق can_discover_company كي لا تكشف الأعداد أو الترتيب نشاط رحلات خاصة بصورة غير مباشرة.

لا تشارك استجابة خاصة في CDN أو مخبأ مشترك؛ تحمل private, no-store. وحدها استجابة PUBLIC تحمل تخزيناً عاماً، وتضيف كل الاستجابات Vary: Origin, Authorization, Cookie. وجود credential خاص غير صالح أو منتهي يعيد 401 قبل استعلام التجميع ولا يتراجع إلى بيانات الإنتاج العامة. يشمل حل المجال Bearer والكوكي HttpOnly الخاص وجلسة @supabase/ssr المقسمة إلى أجزاء، لذلك لا يمكن تصنيف جواب خاص على أنه عام بسبب اختلاف وسيلة تسجيل الدخول.

تبقى واجهات التسويق ذات المخبأ المشترك (/landing/routes) وملف sitemap عامة حصراً وتستبعد شركات is_internal وis_demo، والشركات الموقوفة أو غير النشطة أو غير المنشورة، صراحة. كما يتحقق مسار مراجعات الشركة من إسقاط public_companies قبل استخدام service role، ودالة get_public_company_profile الآمنة ذاتياً تستبعد المجالين داخل الدالة لأنها SECURITY DEFINER ولا يمكنها الاعتماد على RLS.

البحث القانوني عن Journey

POST /journeys/search
Content-Type: application/json

هذا أمر بحث عام للقراءة فقط رغم استخدام POST لحمل معايير البحث المركبة؛ لذلك لا يحتاج جلسة أو cookie أو ترويسة CSRF. يبقى محمياً بحد الطلبات والتحقق الصارم من المخطط، ولا يجوز توسيع هذا الاستثناء إلى أي أمر يغيّر البيانات. يتيح هذا العقد نفسه للويب وتطبيق Flutter Web البحث كضيف من نطاق مختلف من دون إنشاء جلسة مصطنعة.

رغم أن الجلسة ليست مطلوبة، يحتفظ المسار بهوية bearer/cookie عندما تكون موجودة كي تطبق قاعدة البيانات مجال مخزون واحداً. الضيف والحساب العادي يريان رحلات الإنتاج الحقيقية فقط، والحساب الداخلي يرى رحلات QA الداخلية فقط، وشخصية العرض ترى رحلات شركة demo_company_id المطابقة فقط. لا تمزج الاستجابة هذه المجالات، ولا يكفي تخمين Trip UUID لتجاوز القاعدة عند إنشاء quote أو قفل مقعد.

يجب إرسال واحد فقط من origin_city_id أو origin_stop_id، وواحد فقط من destination_city_id أو destination_stop_id. مثال بحث محطة إلى مدينة:

{
"origin_stop_id": "uuid",
"destination_city_id": "uuid",
"departure_date": "2026-08-23",
"passengers": 1,
"locale": "ar",
"currency": "SYP"
}

يحافظ رابط الويب/الموبايل القابل للمشاركة على المجال باستخدام fromType=stop أو fromType=city وtoType. الروابط القديمة التي لا تحمل النوع تعامل كبحث مدينة للتوافق، ولا تُخمن كمحطة.

كل leg.board وleg.alight في النتيجة يميز صراحةً بين هويتين:

{
"id": "trip-stop-call-uuid",
"stop_call_id": "trip-stop-call-uuid",
"stop_id": "physical-stop-uuid",
"name_ar": "نقطة الصعود"
}
  • stop_call_id هو النداء المحدد لهذه الرحلة ويجب إرساله إلى التسعير والحجز.
  • stop_id هو موقع التوقف الدائم ويستخدم للعرض والخريطة والتنقل.
  • id اسم توافق خلفي لـstop_call_id كي تبقى إصدارات العملاء المنشورة آمنة؛ يجب ألا تعتمد الشيفرة الجديدة على هذا الاسم العام.

إنشاء عرض سعر Journey

POST /journeys/quote
Content-Type: application/json

يستهلك المسار هويات Stop Call التي أعادها البحث نفسه. لا يجوز إرسال stop_id مكانها لأن الرحلة قد تزور الموقع نفسه أكثر من مرة. مثال مقطع واحد:

{
"party_size": 1,
"locale": "ar",
"currency": "SYP",
"legs": [
{
"trip_id": "uuid",
"board_stop_call_id": "uuid",
"alight_stop_call_id": "uuid"
}
],
"resources": [
{
"leg_sequence": 1,
"resource_code": "SEAT",
"quantity": 1,
"assignments": []
}
],
"idempotency_key": "stable-client-key"
}

يتحقق اختبار قاعدة البيانات من التسلسل الكامل search → quote كي لا تنجح العقود منفصلة بينما تفشل عند تسليم هوية المحطة بينهما.

تتحقق الخدمة من البنية القانونية لمعرفات UUID كما يقبلها نوع uuid في PostgreSQL، ولا تفترض رقم إصدار RFC محدداً. يضمن ذلك توافق رحلات QA القديمة ذات المعرفات الحتمية من الإصدار الصفري، مع بقاء النص غير القانوني مرفوضاً قبل أي كتابة أو حجز للمخزون.

تثبيت الموارد وتأكيد Journey أو Journey Order

POST /journeys/{bookingId}/hold
DELETE /journeys/{bookingId}/holds/{holdBatchId}
POST /journeys/{bookingId}/confirm
GET /journeys/{bookingId}

POST /journey-orders/{orderId}/hold
DELETE /journey-orders/{orderId}/holds/{holdBatchId}
POST /journey-orders/{orderId}/confirm
GET /journey-orders/{orderId}

تستهلك أوامر confirm عرض السعر والجلسة ودفعة الحجز وبيانات المسافرين ومفتاح idempotency واحداً. بعد نجاح التثبيت قد تعيد الاستجابة confirmation.status=CONFIRMED أو REQUESTED. الحالة الثانية تعني أن المخزون محجوز وأن الطلب وصل إلى الشركة، لكنها لا تعني إصدار صلاحية صعود: تكون كل التذاكر PENDING_CONFIRMATION بلا payload أو signature، وتكون boarding_authorized=false.

بعد موافقة كل الشركات المطلوبة تصبح التذاكر موقعة وتتحول الصلاحية إلى true. الرفض أو انتهاء المهلة يلغي النطاق ويحرر الموارد. يجب على العميل عرض confirmation.deadline وfailure_reason وعدم إنشاء QR أو طلب PDF اعتماداً على booking.status وحده. تستخدم قائمة السجل والتفاصيل والويب والموبايل الإسقاط نفسه، ولا يجوز لأي قناة إعادة اشتقاق الصلاحية محلياً.

البحث عن الرحلات

GET /trips

المعاملات تعتمد على المدينة والتاريخ وعدد المسافرين، كما هو مستخدم في بوابة العملاء الحالية.

معاملات مدعومة حالياً:

المعاملالوصف
from أو origin أو origin_city_idمدينة الانطلاق أو معرفها
to أو dest أو dest_city_id أو destination_city_idمدينة الوصول أو معرفها
dateتاريخ الرحلة
passengersعدد الركاب

تقبل الواجهة أسماء الحقول القديمة والجديدة حتى لا تنكسر تطبيقات الويب والموبايل أثناء الانتقال بين from/to وحقول *_city_id.

هذا مسار توافق للعملاء الأقدم، لكنه يطبق حدود المخزون نفسها التي يطبقها بحث Journey القانوني. يتحقق الخادم من JWT قبل قراءة app_metadata: الطلب العام يقرأ public_trips للشركات النشطة والمنشورة وغير الموقوفة فقط، والحساب الداخلي يقرأ الشركات المنشورة ذات is_internal=true فقط، وشخصية العرض تقيد الاستعلام بشركة العرض المنشورة والنشطة المطابقة فقط. وجود credential خاص منتهي أو غير صالح يعيد 401 ولا يرجع بصمت إلى مخزون الإنتاج العام. لا يعتمد القرار أبداً على user_metadata القابل لتعديل العميل.

تحمل كل نتيجة وتفاصيل GET /trips/{id} كائناً pickupLocation من snapshot الرحلة: id, name, nameEn, address, addressEn, latitude, longitude, directions, وdirectionsEn. يحتفظ /trips أيضاً بالحقول pickup_* المسطحة مؤقتاً لعملاء Flutter المرحّلين. لا يجوز للعميل استبدال هذه القيم بإحداثيات المدينة أو محطة افتراضية.

المسارات المفضلة

GET /favorites
POST /favorites
DELETE /favorites?originCityId=<uuid>&destCityId=<uuid>

هذه عائلة خاصة بالمسافر وليست قائمة عامة. تتطلب العمليات الثلاث جلسة مسافر صالحة، وتتطلب عمليتا POST وDELETE ترويسة CSRF في الويب. لا يستعمل الخادم معرف مسافر مرسلاً من العميل؛ يحل الهوية القانونية ثم يقيد كل قراءة وكتابة بـpassenger_id الخاص بها. استجابة GET موسومة دائماً Cache-Control: private, no-store.

جسم الإضافة:

{
"originCityId": "11111111-1111-4111-8111-111111111111",
"destCityId": "22222222-2222-4222-8222-222222222222"
}

تعيد القراءة أحدث المسارات أولاً بعقد واحد للويب والموبايل:

{
"favorites": [
{
"id": "uuid",
"origin_city_id": "uuid",
"dest_city_id": "uuid",
"created_at": "2026-08-24T08:00:00.000Z",
"origin_city": {
"id": "uuid",
"name_ar": "دمشق",
"name_en": "Damascus"
},
"dest_city": {
"id": "uuid",
"name_ar": "حلب",
"name_en": "Aleppo"
}
}
]
}

يتحقق العميل أيضاً من تطابق معرف المدينة المتداخلة مع معرف العلاقة؛ العقد الناجح المشوه خطأ وليس قائمة فارغة. الإضافة idempotent وتعيد alreadySaved: true عند التكرار. تمنع الواجهة والخادم مدينةً واحدةً لطرفي المسار.

رموز الخطأ المستقرة ذات الأولوية:

HTTPerrorCodeالمعنى
401AUTH_REQUIREDلا توجد جلسة مسافر صالحة
409TRAVELER_PROFILE_REQUIREDالهوية موجودة وملف المسافر غير مكتمل
400CITY_IDS_REQUIREDأحد معرفي المدينتين مفقود
400INVALID_CITY_IDمعرف ليس UUID قانونياً
400ROUTE_CITIES_MUST_DIFFERمدينة الانطلاق والوصول واحدة
400FAVORITE_CITY_NOT_FOUNDالمدينة غير موجودة
500FAVORITES_LOOKUP_FAILEDفشل تحميل القائمة
500FAVORITE_SAVE_FAILEDفشل الحفظ
500FAVORITE_REMOVE_FAILEDفشل الحذف

تفاصيل رحلة

GET /trips/{id}

تعيد الواجهة تفاصيل الرحلة بصيغتين للتوافق:

  • حقول الرحلة في أعلى JSON للواجهات الأقدم.
  • نسخة داخل trip للواجهات الأحدث التي تتوقع { trip }.

تطبق التفاصيل والمقاعد الحدود نفسها. يعاد 404 عند محاولة حساب عام فتح رحلة داخلية/تجريبية، أو محاولة حساب داخلي/تجريبي تخمين UUID من مجال مخزون آخر.

مقاعد الرحلة

GET /trips/{id}/seats

الحجوزات

GET /bookings
GET /bookings/{id}
POST /bookings
POST /bookings/round-trip
PATCH /bookings/{id}
POST /bookings/{id}/cancel

إنشاء حجز واستخدام قسيمة ولاء

الحقل reward_code اختياري في الحجز الفردي والذهاب والعودة، ويقبل رمز استبدال بالصيغة RWD-XXXXXXXX. لا ترسل رمز المكافأة الأصلي من كتالوج المكافآت؛ يجب استبداله أولاً عبر /loyalty/redeem للحصول على رمز القسيمة.

{
"trip_id": "uuid",
"passenger_name": "أحمد محمد",
"passenger_phone": "+963912345678",
"seats_count": 1,
"seat_numbers": ["1D"],
"seat_lock_session_id": "uuid",
"reward_code": "RWD-A1B2C3D4"
}

يتحقق الخادم من سعر المقاعد ومن ملكية القسيمة وشروطها، ثم يحجز المقعد ويعلّم القسيمة مستخدمة داخل معاملة PostgreSQL واحدة. تحتوي استجابة النجاح على السعر النهائي والخصم المطبق. عند رفض القسيمة يرجع 409 مع errorCode ثابت يبدأ عادة بـ LOYALTY_، ولا يجب عرض نص error الخام للمستخدم.

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

تعديل رحلة الحجز ومقاعده

PATCH /bookings/{id}
Content-Type: application/json
X-CSRF-Token: <cookie-matched-token>
{
"action": "modify",
"newTripId": "uuid",
"newSeatNumbers": ["1A", "1B"]
}
  • يقبل العقد أسماء المقاعد النصية الفعلية، بما فيها مخططات الشركات المخصصة؛ لا ترسل أرقاماً ترتيبية من الواجهة الجديدة.
  • يجب إبقاء عدد المقاعد والشركة ومدينتي المسار كما هي، وأن يكون الحجز غير مدفوع وغير مسجل الصعود، وأن يبقى على المغادرة أكثر من 6 ساعات.
  • ينفذ modify_booking_atomic فحص التعارض والتسعير وتحديث سعة الرحلتين وتدوير رموز QR في معاملة واحدة.
  • أخطاء المجال تظهر في errorCode ثابت مثل SEAT_OCCUPIED أو DEPARTURE_TOO_CLOSE؛ لا تعرض الواجهة نص error الخام للمستخدم.

التذاكر والدعم

GET /tickets
GET /tickets/{id}
POST /tickets
GET /bookings/{id}/document?type=ticket&locale=ar
GET /bookings/{id}/document?type=confirmation&locale=en

مسارا document يتطلبان جلسة العميل وملكية الحجز. ticket متاح فقط لحجز صالح وبرمز QR غير منتهي وغير مستخدم؛ يرجع 410 للرمز المنتهي أو المستهلك. أما confirmation فيصدر snapshot immutable ويحتوي اعتماد الصعود الموقّع لكل مسافر كـQR مستقل. هذا QR هو اعتماد الصعود الذي يتحقق منه تطبيق السائق، وليس رابط صفحة عامة. ترجع الاستجابة PDF خاصاً بـno-store وتكتب checksum في document_render_events قبل التنزيل. تتضمن تفاصيل الحجز وقائمة الحجوزات trip.pickupLocation من الاستعلام نفسه بلا قراءة إضافية، ويطبع PDF الاسم والعنوان والإرشادات وإحداثيات/رابط الخريطة.

مشاركة مستند واحد بين جهازين

POST /document-shares
POST /document-shares/{grantId}/preview
POST /document-shares/{grantId}/accept
GET /document-shares/{grantId}/session
GET /document-shares/{grantId}/document?action=view|download&locale=ar|en
POST /document-shares/{grantId}/revoke

إنشاء المنحة وإلغاؤها يتطلبان جلسة مصادقة. يرسل الإنشاء bookingId, documentKind, وpresentation فقط؛ يحل الخادم actor والشركة والملكية/التعيين من subject الموثق. الأنواع المدعومة هي BOOKING_CONFIRMATION, INVOICE, CANCELLATION_CONFIRMATION, وPAYMENT_RECEIPT، بينما يقتصر السائق المعيّن على التأكيد والإيصال. يقتصر INVOICE وPAYMENT_RECEIPT لمستخدم الشركة على OWNER وADMIN؛ يستطيع DISPATCHER وSTAFF مشاركة تأكيدات الحجز/الإلغاء فقط، ولا يحصل مدير منصة من دور SUPPORT على issuer scope. لا يقبل endpoint actor id أو actor role أو company id من العميل.

تعيد استجابة الإنشاء رابطاً من الشكل /share/{grantId}#token={one-use-secret}. ترسل preview وaccept السر في body؛ لا يرسله المتصفح إلى الخادم عند فتح الرابط. يعرض preview نوع المستند وشركة التشغيل فقط، ثم يصدر accept جلسة recipient في cookie من نوع HttpOnly وSameSite Strict. المنحة صالحة 5 دقائق ولمرة واحدة، والجلسة المقبولة صالحة حتى 30 دقيقة مع انتهاء بعد 15 دقيقة خمول. لا تتيح الجلسة أي Booking API أو boarding action.

يفصل تنزيل المستند بين authorize وfinalize: يصيّر الخادم الـsnapshot أولاً، ثم يعيد التحقق من الجلسة ويسجل حدث العرض/التنزيل مع checksum والحجم قبل إرسال البايتات. كل الاستجابات private, no-store مع noindex وnosniff. يفرض الخادم سقفاً واسعاً لكل عنوان IP وسقفاً أدق للمجموعة IP + grantId؛ هذا يحمي نقاط capability العامة من التخمين ولا يجعل مستخدمي شبكة جوال خلف carrier-grade NAT يستهلكون ميزانية بعضهم. ويُقبل action=download فقط عندما تكون presentation=DOWNLOAD في المنحة الموقعة في قاعدة البيانات.

المدفوعات والولاء

GET /payments
POST /payments/create
GET /bookings/{id}/receipt?locale=ar
GET /loyalty/account?companyId={companyId}
GET /loyalty/rewards?companyId={companyId}
GET /loyalty/transactions?companyId={companyId}&limit=20&offset=0
GET /loyalty/referral?companyId={companyId}
POST /loyalty/redeem
POST /loyalty/preview
POST /loyalty/referral

تقبل مسارات إنشاء الدفع CASH فقط. تعيد SHAMCASH وCARD حالة 409 مع ONLINE_PAYMENTS_DEVELOPMENT_ONLY وقائمة available_methods التي تحتوي CASH، بينما تعيد الوسيلة غير المعروفة 400. لا ينشئ وضع التطوير تحصيلاً حقيقياً ولا يتصل بمزود. تفاصيل الفوترة المهنية اختيارية ومحجوبة عن الحجز النقدي؛ تستخدم فقط مع تدفق بطاقة مستقبلي بعد اعتماده.

GET /bookings/{id}/receipt لا يصدر ملفاً قبل إثبات التحصيل. يتحقق من مالك الحجز وتطابق شركة الحجز والمسار، ويختار سجل التحصيل الأعلى موثوقية، ثم يستدعي issue_financial_document لإصدار snapshot ورقم إيصال/استرداد ثابتين. يعاد التصيير من snapshot لا من صف دفع متغير، ويفشل الطلب إذا تعذر تسجيل checksum الملف في سجل التصيير.

كل مسارات حساب الولاء والاستبدال والمعاينة تتطلب جلسة عميل. قائمة البرامج في GET /loyalty/account تعرض برامج الشركات المنشورة، ثم ينشئ اختيار الشركة حساباً منفصلاً عند الحاجة. يقرأ GET /loyalty/rewards فقط مكافآت discount وfree_trip الفعالة والقابلة للاستبدال لدى الشركة المختارة. لا تقبل أي عملية دمج رصيد أو مكافأة بين شركتين.

استبدال النقاط

POST /loyalty/redeem
Content-Type: application/json
X-CSRF-Token: <cookie-matched-token>
{
"companyId": "11111111-1111-4111-8111-111111111111",
"rewardCode": "WELCOME10"
}

استجابة النجاح:

{
"success": true,
"redemption": {
"id": "uuid",
"code": "RWD-A1B2C3D4",
"pointsUsed": 100
}
}

تقفل الدالة حساب الولاء وصف المكافأة داخل الشركة نفسها قبل خصم النقاط وتحديث عداد الاستبدال وإنشاء الحركة والقسيمة. يمنع ذلك إنفاق الرصيد نفسه بطلبين متزامنين أو استخدام مكافأة شركة في حساب شركة أخرى.

معاينة الخصم قبل الحجز

POST /loyalty/preview
Content-Type: application/json
X-CSRF-Token: <cookie-matched-token>
{
"redemptionCode": "RWD-A1B2C3D4",
"subtotal": 150000,
"tripIds": ["uuid"]
}

يمكن أن تحتوي tripIds على رحلة واحدة أو رحلتي ذهاب وعودة. تتحقق المعاينة من المالك والحالة والانتهاء والحد الأدنى والمسارات والأيام المسموحة، لكنها لا تستهلك القسيمة. السعر النهائي داخل دالة إنشاء الحجز هو المرجع عند وجود تغير متزامن.

{
"valid": true,
"redemptionCode": "RWD-A1B2C3D4",
"discount": 15000,
"finalTotal": 135000
}

رموز المجال المتوقعة تشمل:

  • LOYALTY_REDEMPTION_NOT_FOUND
  • LOYALTY_REDEMPTION_USED
  • LOYALTY_REDEMPTION_EXPIRED
  • LOYALTY_REWARD_UNSUPPORTED
  • LOYALTY_MINIMUM_NOT_MET
  • LOYALTY_ROUTE_NOT_ELIGIBLE
  • LOYALTY_DAY_NOT_ELIGIBLE
  • LOYALTY_REWARD_CONFIGURATION_INVALID

دوال PostgreSQL التي تنفذ الاستبدال والمعاينة والحجز ليست متاحة مباشرة إلى anon أو authenticated؛ تستدعيها مسارات الخادم بعد حل هوية الراكب من الجلسة.