واجهات برمجة بوابة العملاء
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 عند التكرار. تمنع الواجهة والخادم مدينةً واحدةً لطرفي
المسار.
رموز الخطأ المستقرة ذات الأولوية:
| HTTP | errorCode | المعنى |
|---|---|---|
| 401 | AUTH_REQUIRED | لا توجد جلسة مسافر صالحة |
| 409 | TRAVELER_PROFILE_REQUIRED | الهوية موجودة وملف المسافر غير مكتمل |
| 400 | CITY_IDS_REQUIRED | أحد معرفي المدينتين مفقود |
| 400 | INVALID_CITY_ID | معرف ليس UUID قانونياً |
| 400 | ROUTE_CITIES_MUST_DIFFER | مدينة الانطلاق والوصول واحدة |
| 400 | FAVORITE_CITY_NOT_FOUND | المدينة غير موجودة |
| 500 | FAVORITES_LOOKUP_FAILED | فشل تحميل القائمة |
| 500 | FAVORITE_SAVE_FAILED | فشل الحفظ |
| 500 | FAVORITE_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_FOUNDLOYALTY_REDEMPTION_USEDLOYALTY_REDEMPTION_EXPIREDLOYALTY_REWARD_UNSUPPORTEDLOYALTY_MINIMUM_NOT_METLOYALTY_ROUTE_NOT_ELIGIBLELOYALTY_DAY_NOT_ELIGIBLELOYALTY_REWARD_CONFIGURATION_INVALID
دوال PostgreSQL التي تنفذ الاستبدال والمعاينة والحجز ليست متاحة مباشرة إلى
anon أو authenticated؛ تستدعيها مسارات الخادم بعد حل هوية الراكب من الجلسة.