الإشعارات المعاملاتية
الحدود المعمارية
رمز OTP جزء من الهوية، وليس إشعاراً عاماً. يرسل OTP عبر SMS فقط من خلال
OTP_PROVIDER=twilio أو عقد البوابة الخارجية المعتمدة. لا تستخدم مسارات OTP
Meta أو WhatsApp، ولا تنتقل إلى تيليجرام تلقائياً عند فشل SMS. تيليجرام اختيار
مستقل يطلبه المستخدم صراحة.
أما رسائل الحجز والرحلة والدفع فتستخدم control plane معاملاتياً مستقلاً:
business transaction
-> enqueue_notification_once(...)
-> notifications + notification_deliveries
-> worker claim with SKIP LOCKED
-> provider adapter
-> immutable attempt + provider event
-> aggregate notification state
لا تنفذ معاملة الحجز اتصالاً شبكياً بمزود البريد أو SMS. تكتب نية الإرسال داخل PostgreSQL أولاً؛ لذلك لا يضيع الإشعار عند إعادة تشغيل التطبيق بعد نجاح معاملة الأعمال.
عقود البيانات
notification_templates: النسخة النشطة والـcriticality وحالة الاعتماد.notification_template_versions: تاريخ نسخ القالب غير القابل للطمس.notifications: الحدث المنطقي، tenant، اللغة، correlation/causation، والقنوات المطلوبة.notification_deliveries: دورة مستقلة لكل قناة ومستلم مع idempotency ومواعيد retry وقفل worker.notification_delivery_attempts: محاولة مزود واحدة مع النتيجة والمدة والخطأ المنقح.notification_delivery_events: callbacks مزودين idempotent بحسب معرف حدث المزود.notification_fanout_jobs: توسعة أحداث الرحلات الكبيرة على دفعات محدودة.
تخزن delivery rows نسخة masked وhash للمستلم لأغراض التشغيل، ولا يجوز إرسال
رقم كامل أو OTP أو API key إلى logs أو Slack. شركات العرض التجريبي تسجل delivery
محاكياً داخل demo_outbox ولا تتصل بمزود خارجي.
الحالات وإعادة المحاولة
يملك worker وحده provider I/O. يدعي دفعة تحت lock token، ويكتب محاولة مرقمة، ثم
ينهي delivery عبر RPC يتحقق من القفل. الأخطاء المؤقتة تستخدم backoff مع
next_attempt_at؛ الخطأ الدائم أو استنفاد المحاولات ينقل delivery إلى
dead_lettered. callbacks قد ترفع الحالة من accepted إلى sent أو delivered أو
read، لكنها لا تعيد حالة نهائية إلى الوراء.
صلاحيات العامل قائمة تواقيع صريحة وليست GRANT جماعياً على كل دوال public.
يسحب migration 00264 تنفيذ get_pending_reminders وmark_reminder_sent من
PUBLIC وanon وauthenticated وservice_role لأنهما ينتميان إلى مسار
الإرسال المباشر المتقاعد، ثم يعيد تثبيت أوامر outbox وfan-out الدائمة المطلوبة
فقط. تهيئة Docker لا تعيد كتابة ACLs التي تملكها migrations عند إعادة التشغيل.
الحالات الحرجة مثل إلغاء الرحلة وتأخيرها يمكن أن تطلب أكثر من قناة. تعطيل قناة
من البيئة ينتج suppressed صريحاً، لا نجاحاً وهمياً. حالة notification الكلية
تُشتق من حالات القنوات (SENT, DELIVERED, PARTIAL, FAILED) ولا يقررها
adapter منفرد.
حوكمة القوالب
النص العربي والإنجليزي مصدره قالب معتمد واحد. renderer المشترك يبني نصاً بسيطاً
وHTML بريد متجاوباً وRTL/LTR؛ adapters لا تملك نسخاً مستقلة من الصياغة. تدعم
التفضيلات ar, en, وtr، لكن التركية تسقط إلى الإنجليزية حتى تعتمد ترجمة
تركية كاملة، كي لا تختلط عربية RTL في تجربة تركية مستقبلية.
أي تعديل نص معاملات يجب أن يزيد version، يسجل change note واعتماداً، ويضيف اختباراً للمتغيرات الناقصة والـescaping والروابط. لا يعاد استخدام قالب تسويقي لرمز هوية أو إشعار أمني.
تمر كل رسالة مرتبطة برحلة عبر enqueue_templated_notification(...). إذا حمل
الحدث trip_id، يثري هذا seam المتغيرات من snapshot الرحلة نفسه:
pickup_name, pickup_address, pickup_directions, وpickup_map_url. لا
تستعلم adapters عن جدول المواقف ولا تبني كل قناة مكاناً مختلفاً. هكذا تستخدم
رسالة التأكيد والتذكير والتأخير والإلغاء المكان نفسه الذي تعرضه لوحة الشركة
والحجز وPDF. يظل search_path للدالة مقيداً بـ
pg_catalog, public, extensions حتى يعمل digest() من pgcrypto بأمان.
تستخدم قنوات المسافر الخارجية action موحداً وآمناً إلى /find-booking؛ لا يصبح
رابط UUID الحجز capability. بعد إدخال رمز الحجز واسم العائلة تصدر جلسة ضيف
محدودة للحجز، أو يسجل المسافر الدخول لعرض الحجوزات التي يملكها. العرض مخصص للقناة:
- بريد
booking_confirmedيرفق PDF تأكيد الحجز الثابت فعلياً، بما فيه QR الموقّع لكل مسافر، ويعرض زر البحث الآمن ورابطاً نصياً بديلاً. - SMS وWhatsApp fallback يضيفان رابط البحث المطلق؛ ويمكن لقالب WhatsApp المعتمد
طلب
ticket_urlضمن parameter keys، لكنه لا يحتوي سراً أو UUID قابلاً للتخمين. - push وin-app يحتفظان بالـdeep link الأصلي؛ عند فتحه يفرض تطبيق المسافر ملكية الحساب ولا يثق بالرابط وحده.
لا ينسخ worker توقيع QR أو payload التذكرة إلى body أو provider metadata. يبقى الاعتماد داخل snapshot المستند ومرفق التأكيد وصفحة التذكرة المحمية فقط، ما يقلل التسرب عبر سجلات مزودي الاتصال.
التحقق التشغيلي
الترحيل يضيف عقود PostgreSQL للـclaim والتكملة والـfan-out والcallbacks. يشغّل
notification-worker هذه العقود دورياً بقيم batch/concurrency محدودة. النشر
يرفض إعداد قناة مفعلة بلا credentials أو callback secret صالح، وينتظر health
worker قبل إعلان اكتمال النشر.
الاختبار المرجعي:
pnpm --filter @shambus/services test -- --run \
__tests__/notification-delivery-worker.service.test.ts \
__tests__/notification-delivery-handlers.service.test.ts \
__tests__/notification-template.service.test.ts \
__tests__/notification-webhook.service.test.ts
bash infrastructure/scripts/test-database-contracts.sh