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

المستندات وملفات PDF

الهدف

تستخدم التذكرة وتأكيد الحجز وإيصال الدفع/الاسترداد والتقرير التشغيلي renderer واحداً وهوية مرئية واحدة. لا تولد صفحات React ملفات PDF من المتصفح، ولا يبني كل تطبيق نسخة مختلفة من النص أو الألوان أو اتجاه المسار.

authenticated route
-> ownership / tenant / role check
-> immutable source snapshot (booking and financial documents)
-> canonical self-contained HTML
-> private Gotenberg Chromium renderer
-> PDF magic/size validation
-> SHA-256 render event
-> private no-store download

أنواع المستند

النوعالمصدرQRالاتجاهالسجل
تذكرة الصعودالحجز + الرحلة + QR صالحنعمAR/ENrender event
تأكيد الحجزsnapshot الحجز الجماعي والتذاكرلكل مسافرAR/ENbooking document + render event
الفاتورةsnapshot الحجز والتسعير والتحصيللاAR/ENbooking document + render event
تأكيد الإلغاءsnapshot الإلغاء والاستردادلاAR/ENbooking document + render event
إيصال الدفعsnapshot تحصيل مكتمللاAR/ENfinancial document + render event
إشعار الاستردادsnapshot عملية مستردةلاAR/ENfinancial document + render event
تقرير تشغيليعقد التقرير المجمعلاAR/ENrender event

تعرض نقطة الانطلاق والوصول في كتلتين مسمّاتين؛ لا تستخدم سهماً قد ينعكس خطأ في RTL. كل قيمة غير موثوقة escaped، وQR يقبل SVG خاملاً فقط، ولا يحتوي HTML على صور أو خطوط أو CSS بعيدة. هذا يمنع تحويل renderer إلى SSRF client. يولّد route الخادم SVG بمولّد QR غير مرتبط بـReact، مع error correction مستوى H وحد صلب 1024 byte للـpayload قبل التصيير. هذا يبقي route handler ضمن عقد Next.js الخادم ويحد من الذاكرة/العمل المستهلك من سجل QR غير سليم.

السجل المالي الثابت

issue_financial_document يعمل بـservice role بعد تحقق API من المستخدم والشركة. يحل سجل TRANSACTION, DRIVER, OFFICE, أو BOOKING إلى snapshot واحد ويصدر رقماً ثابتاً:

  • SB-R-YYYYMM-######## لإيصال الدفع.
  • SB-CN-YYYYMM-######## لإشعار الاسترداد.

الجدول financial_documents يرفض UPDATE وDELETE ويملك uniqueness على المصدر والنوع، لذلك إعادة الطلب idempotent. لا يعاد بناء الإيصال من صف دفع متغير بعد الإصدار. يسجل snapshot_sha256 حقيقة الإصدار، ثم يسجل كل تصيير في document_render_events مع checksum الملف وحجمه ونسخة القالب وtrace renderer. إذا فشل audit insert لا يعيد API ملفاً مالياً غير موثق.

مستندات الحجز الثابتة

يصدر issue_booking_document أحد الأنواع BOOKING_CONFIRMATION, INVOICE, أو CANCELLATION_CONFIRMATION بعد تحقق الشركة والدور وحالة الحجز. يبني snapshot واحداً من aggregate رحلة العميل، وليس من صف bookings المسطح فقط. تشمل اللقطة:

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

في BOOKING_CONFIRMATION فقط، يضيف builder خاص بالمستند إلى كل تذكرة النسخة الموقعة من credential_version, credential_payload, وcredential_signature. يحوّل route هذه الحقيقة إلى QR مستقل لكل مسافر؛ وهو نفس العقد الخالي من PII الذي يتحقق منه driver scanner. لا تظهر التواقيع في read model العادي للوحة، ولا تضاف إلى الفواتير أو إشعارات الإلغاء. تبقى snapshots القديمة قابلة للتصيير بلا تعديل، بينما يصدر الطلب الحالي مراجعة template v2 قابلة للتدقيق.

تمر كتابات التوافق القديمة من مساري الحجز للعميل والمكتب عبر sync_legacy_booking_canonical_artifacts. ينشئ الإسقاط مقعد Journey عند الحجز، ولا يصدر التذكرة الموقّعة إلا عند التأكيد، ثم يزامن حالات الصعود والإكمال وعدم الحضور والإلغاء. العملية idempotent ويمنع trigger التوافق إعادة تنشيط hold أثبتت جلسته أنه منتهٍ. مسار Journey الأصلي لا يطابق source التوافق ولا يمر بهذا الإسقاط.

تحفظ booking_documents الرقم والمراجعة وsnapshot_sha256 وعلاقة supersedes_document_id. إعادة تنزيل نفس الحقيقة idempotent؛ إذا تغيرت الحقيقة القابلة للإصدار تنشأ مراجعة جديدة ولا تمحى السابقة. يقبل route معامل documentId لتنزيل snapshot التاريخي المحدد، ويتحقق في الاستعلام نفسه من الشركة والحجز ونوع المستند. لا يعيد بناء مراجعة قديمة من بيانات اليوم.

تعيد get_company_booking_workspace read model واحداً للوحة الشركة، مع المستندات وإيصالات التحصيل وسجل الاتصالات وأحداث التصيير. يتحقق API من هذا العقد عبر Zod قبل إرساله؛ يفشل مغلقاً إذا كانت أي علاقة أساسية ناقصة بدلاً من عرض صفحة جزئية.

الترصيص وإتاحة الوصول

خدمة gotenberg داخل شبكة Compose فقط، بلا Traefik route أو منفذ إنتاج عام. الصورة مثبتة على Gotenberg 8.34.0 وdigest ثابت، وتضيف خط Cairo المرخص داخل الحاوية. يرسل adapter limits للوقت وحجم HTML وحجم PDF، ويتحقق من Content-Type وبداية %PDF- قبل قبول النتيجة.

Chromium يولد PDF tagged وdocument outline مباشرة. لا تستخدم مسارات المنتج حالياً تحويل PDF/A اللاحق: Gotenberg ينفذه عبر LibreOffice، وكشف QA بصري حقيقي على 8.34.0 أنه يقلب أرقاماً وتواريخ عربية داخل RTL. تمنع الخدمة PDF/A صراحة لأي مستند RTL بـPDF_ARCHIVE_RTL_UNSAFE بدلاً من إصدار دليل مالي مضلل. يمكن إبقاء التحويل opt-in لمستند LTR بعد اختبار بصري مستقل؛ ولا يجوز وصف PDF عادي بأنه PDF/A.

الوصول والأخطاء

  • المسافر المسجّل لا ينزل إلا مستند حجز مرتبطاً بهوية حسابه. المسار البديل للضيف يتطلب رمز الحجز مع snapshot اسم العائلة، ثم يصدر جلسة opaque خاصة بالحجز في cookie من نوع HttpOnly وSameSite؛ تنتهي بعد 15 دقيقة خمول أو 30 دقيقة مطلقة.
  • أوامر تأكيد Journey وJourney Order التي تحفظ اسم العائلة تعمل عبر customer API الموثوق فقط. أدوار anon وauthenticated لا تملك تنفيذ دوال SECURITY DEFINER مباشرة؛ يبقى checkout بلا حساب ممكناً عبر عقد API المحدود والمتحقق منه.
  • UUID الحجز أو رابط التذكرة لا يمنح أي صلاحية. جلسة الضيف تتيح تأكيد الحجز فقط، ولا تفتح live ticket أو حجوزات أخرى. يعاد الخطأ العام نفسه عند عدم التطابق.
  • تذكرة الصعود متاحة فقط للحجز الصالح وQR غير منتهي وغير مستخدم؛ تأكيد الحجز الحالي يحمل رمز الصعود الصالح لكل مسافر، ولا يعيد إظهار رمز legacy مستخدم أو منتهي.
  • الإيصال لا يصدر قبل إثبات التحصيل، ويتحقق tenant من الحجز والمسار والشركة.
  • لوحة الشركة تقيد الإيصال بأدوار الإدارة المالية للشركة النشطة والمتحققة.
  • كل download يحمل private, no-store, nosniff واسم ملف منقح.
  • تمر لوحة الشركة وتنزيل المسافر ومرفق البريد عبر generateBookingDocumentPdf: إصدار snapshot، إنشاء QR، التصيير، وتسجيل render evidence عملية واحدة لا يمكن لسطح التسليم تجاوز جزء منها.
  • فشل renderer مؤقتاً يرجع 503؛ بيانات غير PDF أو رفض renderer يرجع 502؛ تجاوز الحجم يرجع 413.

التسليم والمشاركة الآمنة

الطريقة الأساسية للمشاركة هي تنزيل ملف PDF الفعلي الصادر من الخادم ثم تمرير الملف إلى ورقة المشاركة/الطباعة الأصلية في نظام التشغيل. لا يشارك العميل رابط صفحة الحجز، ولا ينسخ اسم المسافر أو هاتفه أو تفاصيل الرحلة إلى رسالة نصية. هذا يبقي البريد وتطبيقات الرسائل والطباعة على النسخة immutable نفسها، ويتيح للمستلم الاحتفاظ بنسخة صريحة من المستند.

عند وجود جهازين في المكان نفسه، يوفر الويب خياراً ثانوياً باسم المشاركة القريبة. ينشئ هذا الخيار QR لنقل مستند واحد فقط وفق العقد التالي:

authenticated issuer
-> server resolves Traveler / assigned Driver / Company operator / Admin
-> issue or resolve one immutable document
-> one-use grant (5 minutes)
-> secret exists only in URL fragment and QR
-> recipient sees minimal Company + document-kind preview
-> explicit accept
-> scoped HttpOnly SameSite=Strict recipient session
-> authorize render
-> render and validate PDF
-> revalidate + record checksum-backed delivery
  • لا يمثل QR النقل تذكرة صعود، ولا يقبله ماسح السائق، ولا ينقل ملكية الحجز أو يفتح صفحة إدارته. رمز الصعود الموقّع داخل PDF التأكيد اعتماد مختلف تماماً.
  • لا تخزن قاعدة البيانات secret المنحة أو الجلسة الخام؛ تخزن SHA-256 فقط. وجود السر في fragment يمنع إرساله في request target أو access logs أو Referer أثناء فتح الصفحة.
  • المنحة single-use وتنتهي بعد 5 دقائق ويمكن لمصدرها إلغاؤها. بعد القبول تنتهي جلسة المستلم بعد 15 دقيقة خمول أو 30 دقيقة كحد مطلق، وتقتصر cookie على مسار API الخاص بالمنحة.
  • تستقبل دوال PostgreSQL مدد TTL محدودة فقط وتحسب expires_at من ساعة قاعدة البيانات. لا يرسل تطبيق الويب timestamp نهائياً، لذلك لا يمدد اختلاف الساعة بين الحاويات صلاحية النقل ولا يرفض منحة صحيحة قبل إنشائها.
  • يحل الخادم هوية المصدر من subject المصادق وعلاقات الحجز؛ لا يقبل actor أو company من JSON العميل. يشارك السائق المعيّن تأكيد الحجز وإيصال التحصيل فقط، ولا يشارك فاتورة أو مستنداً خارج رحلته. يستطيع OWNER وADMIN في الشركة إصدار الفاتورة وإيصال الدفع، بينما تبقى أدوار DISPATCHER وSTAFF ضمن مستندات الحجز التشغيلية. لا يدخل مدير المنصة من دور SUPPORT قناة إصدار المستندات. ترجع قاعدة البيانات الدور الفعلي مع هوية المصدر حتى يطبق API هذه الحدود من المصدر الموثوق نفسه، لا من claim يرسله العميل.
  • التفويض والتسجيل مرحلتان. لا يسجل VIEWED أو DOWNLOADED إلا بعد نجاح renderer والتحقق من PDF ثم إعادة فحص الإلغاء والانتهاء. يحمل الحدث checksum وحجم الملف وrenderer trace، لذلك لا ينتج فشل التصيير دليلاً كاذباً على التسليم.
  • كل endpoints الخاصة بالمشاركة no-store, noindex, nosniff ومحدودة المعدل. تستخدم حداً واسعاً لكل IP مع حد أدق لكل IP + grant حتى لا يحجب carrier-grade NAT مستخدمين سوريين غير مرتبطين، مع إبقاء سقف إساءة استخدام عاماً. لا تظهر الأسرار في logs أو analytics أو رسائل الخطأ.
  • إذا أصدر المصدر grant من نوع VIEW فلا تعرض واجهة المستلم إجراء التنزيل، كما يرفض الخادم DOWNLOAD حتى لو صيغ الرابط يدوياً. نوع DOWNLOAD يتيح العرض والتنزيل معاً.
  • عند حذف بيانات عرض تجريبي، تمسح معاملة teardown أحداث المشاركة والجلسة والمنحة قبل المستند الأم بعد التحقق من شركة demo. لا يعمل هذا الاستثناء مع شركة حقيقية ولا يجعل سجل التدقيق قابلاً للتعديل عبر API.

تطبيق العميل يشارك تأكيد الحجز، أو تأكيد الإلغاء للحجز الملغى، كملف PDF عبر share_plus. تطبيق السائق يشارك تأكيد الحجز فقط لحجز على Trip معيّنة له، ويشارك إيصال الدفع بعد ثبوت التحصيل ومزامنته. تعرض ورقة النظام خيار الطباعة عندما يدعمه الجهاز أو مزود الطباعة؛ لا يولد التطبيق إيصالاً محلياً منافساً.

QA والنشر

لتوليد fixtures حقيقية محلياً:

docker compose --profile apps up -d gotenberg
GOTENBERG_URL=http://127.0.0.1:3006 \
pnpm --filter customer qa:documents

عند تشغيل تطبيقات Next.js في وضع التطوير، تستخدم خدمة المستندات تلقائياً http://127.0.0.1:3006، وهو المنفذ المحلي لخدمة Gotenberg في Compose. يمكن تجاوز هذا العنوان صراحةً عبر GOTENBERG_URL. لا يوجد هذا fallback في الاختبار أو الإنتاج؛ تبقى بيئات غير التطوير مغلقة افتراضياً إذا غاب المتغير.

تكتب العينات تحت output/pdf/ (متجاهلة من Git). يجب تشغيل pdfinfo, pdftotext, وpdftoppm ثم فحص كل صفحة بصرياً عند تغيير القالب أو الخط أو renderer. تحقق baseline الحالي من صفحة A4 واحدة للتذكرة والإيصال، ومن صفحتين A4 لكل من تأكيد الحجز الجماعي والفاتورة وتأكيد الإلغاء بالعربية والإنجليزية، وتكرار رأس الجدول في التقرير متعدد الصفحات، وTagged: yes، وصحة أرقام وتواريخ العربية. يجب كذلك فك QR لكل مسافر من fixture تأكيد الحجز ومطابقته مع Ticket ID والنسخة والتوقيع الموجود في snapshot، لا الاكتفاء برؤية مربع QR في الصورة. تحتوي كل صفحة مستند حجز على رقم المستند في التذييل حتى تبقى الصفحة المنفصلة قابلة للمطابقة. الهوية مضمّنة SVG من أصل شعار شام باص ولا تحمل مورداً خارجياً.

النشر يبني renderer تسلسلياً، ينتظر /health، ثم ينفذ تحويل HTML فعلياً ويتحقق من PDF magic والحجم من داخل تطبيق العميل قبل إعلان نجاح smoke tests.

اختبار Playwright customer-document-share ينفذ السلسلة كاملةً عبر Chromium: مستخدم مسجل ينشئ المنحة من صفحة الحجز، مستلم مجهول يفتح QR ويقبل المعاينة، ثم ينزل PDF حقيقياً ويتحقق من magic bytes وheaders، وأخيراً يثبت أن إعادة استعمال الرابط تفشل. يشغّل الاختبار React في وضع التطوير أيضاً لكشف إعادة تشغيل effects في Strict Mode قبل النشر.