المستندات وملفات 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/EN | render event |
| تأكيد الحجز | snapshot الحجز الجماعي والتذاكر | لكل مسافر | AR/EN | booking document + render event |
| الفاتورة | snapshot الحجز والتسعير والتحصيل | لا | AR/EN | booking document + render event |
| تأكيد الإلغاء | snapshot الإلغاء والاسترداد | لا | AR/EN | booking document + render event |
| إيصال الدفع | snapshot تحصيل مكتمل | لا | AR/EN | financial document + render event |
| إشعار الاسترداد | snapshot عملية مستردة | لا | AR/EN | financial document + render event |
| تقرير تشغيلي | عقد التقرير المجمع | لا | AR/EN | render 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 قبل النشر.