محاسب
نظام Laravel متعدّد المستأجرين مع نقطة بيع Flutter offline-first، فوترة إلكترونيّة متوافقة مع ZATCA، ومحرّك مزامنة Reverb يخدم أكثر من 500 منشأة في الخليج.
عن المشروع.
محاسب منصّة محاسبة ونقطة بيع متعدّدة المستأجرين مبنيّة للمنشآت الصغيرة في الخليج. كلّ عميل يحصل على دفاتره ومستخدميه وتطبيق كاشير بـ Flutter يبقى يعمل حين تنقطع الشبكة عن المحلّ. قُدت الهندسة على ثلاث طبقات: واجهة Laravel للخادم، تطبيق Flutter يعمل دون اتّصال، وطبقة WebSocket للمزامنة اللحظيّة بينهما. أصعب جزء كان أن يبقى الكاشير شعورُه فوريًّا على شبكات متذبذبة، بينما تبقى الإدارة دقيقة حتى آخر ريال.
ما الذي تغيّر.
مستأجر على المنصّة
زمن المزامنة، الـ p99
انخفاض في تذاكر الدعم
وقت تشغيل، آخر اثنا عشر شهرًا
ما الذي وجدناه.
شركة محاسبة إقليمية احتاجت منصّة واحدة تستضيف مئات الشركات الصغيرة، كل واحدة بدفاترها ومستخدميها ونقطة بيع تشتغل أوفلاين. الحلول الموجودة كانت إمّا on-premises بمستأجر واحد، أو حزم سحابية صلبة لا تتقن العربية.
المطلوب: ERP متعدّد المستأجرين يعمل في المتصفّح وعلى الهواتف وعلى تابلتات بشاشات مكسورة، ويبقى متّسقًا حين تنقطع الشبكة الساعة الثالثة عصرًا.
ما الذي فعلناه.
Laravel للـ API الخاصّ بالمستأجرين ولنواة المحاسبة، وFlutter لتطبيق نقطة بيع أوفلاين-أوّلًا مع تخزين Drift محلّي، وطبقة WebSocket عبر Reverb للمزامنة اللحظيّة بين أمين الصندوق والإدارة.
نقطة سحب موحَّدة استبدلت 48 نداءً منفصلًا للمزامنة. ضمان عدم تكرار من خمس طبقات على كلّ صفّ ماليّ، ترقيم من جهة الخادم، ومفتاح ميزة يسمح بتفعيل الـ WebSocket لمستأجر بعينه دون إعادة نشر التطبيق.
ما الذي أصبح.
أكثر من خمسمئة مستأجر على المنصّة، انخفاض 78٪ في تذاكر الدعم بعد إطلاق المزامنة الموحَّدة، وزمن مزامنة يبقى تحت 200 مللي ثانية في الـ p99 حتى مع ألف أمين صندوق متزامن.
كيف يعمل فعلًا.
الجزء المثير ليس الـ API نفسه، بل ما يحدث حين يحرّر أمين الصندوق فاتورة بينما اتّصاله ينقطع كلّ خمس عشرة ثانية.
- 01استخدام UUID محلّي للمفاتيح الأجنبيّة وقت الكتابة، وإعادة كتابتها إلى معرّف الخادم الصحيح وقت الدفع فقط. هذا يلغي صنفًا كاملًا من أخطاء «العميل الذي أُنشئ أوفلاين ولم يُزامَن أبدًا».
- 02تحريك مؤشّر الـ delta فقط بعد أن يُحدَّث صفّ واحد على الأقل محلّيًّا. تجاهل هذه القاعدة يضيع البيانات على الدورات التي تخطّت مفاتيحها.
- 03مفتاح idempotency على كلّ دورة، يُعكَس على الخادم، ويُعاد الدفع تحت نفس المفتاح حتى الإقرار.
- 04Reverb للوقت الحقيقيّ، وفولباك بالـ polling خلف مفتاح ميزة، ولا يعملان معًا أبدًا. النسخ القديمة من تطبيق Flutter تستمرّ بالعمل لأن الـ polling هو الأرضيّة لا السقف.
دوري الفعليّ في هذا المشروع.
منفرد · بناء كامل من الألف إلى الياء بنسبة 100٪. لا شركاء مؤسّسين، لا متعاقدين، لا وكالة، لا كود موروث. كلّ سطر كود، كلّ مخطّط قاعدة بيانات، كلّ vhost في nginx، كلّ ترجمة عربيّة، كلّ بكسل في الواجهة — لي. الأدوار السبعة أدناه تصف ما يفعله شخص واحد فعلاً ليُطلِق ويُشغِّل منصّة ERP متعدّدة المستأجرين تخدم +500 شركة حيّة.
مهندس معماريّ للمنتج
النموذج النطاقيّ الكامل: 78 جدولاً عبر 24 وحدة (محاسبة، مخزون، موارد بشريّة، رواتب، نقاط بيع، تصنيع، مشاريع، CRM). استراتيجيّة عزل المستأجرين، تسلسل المزامنة، تصميم تكامل زاتكا المرحلة الثانية.
مهندس باك-إند
Laravel 11 موحّد، +200 controller، +600 نموذج Eloquent. محرّك محاسبة بقيد مزدوج، تقييم مخزون بمتوسّط مرجّح، عملات متعدّدة مع قيود تسوية تلقائيّة، محرّك رواتب، محلِّل قوائم تصنيع.
فرونت-إند × 4 تطبيقات ويب
تطبيق المستأجر (Livewire + Alpine)، سوبر-أدمن (Livewire)، الموقع التعريفيّ (Next.js 14)، باني المتجر (Next.js 14 + ISR). كلّها ثنائيّة اللغة ar/en بـRTL أصليّ — دون ترجمة آليّة أبدًا.
موبايل × 2 تطبيقات Flutter
كاشير POS (أوفلاين-أوّلًا عبر Drift) ومدير الجوّال (موافقات + مقارنة فروع). حالة Riverpod، محرّك مزامنة بـidempotency keys، مرآة محلّيّة من 48 جدولاً.
مصمّم محرّك المزامنة
تسلسل ثلاثيّ الطبقات: WebSocket (Reverb) أساسيّ، Unified Pull ثانويّ، polling تقليديّ كأرضيّة. ضمان عدم تكرار من 5 طبقات. إعادة كتابة UUID→server-ID عند الإرسال. مؤشّر تفاضليّ بأمان تخطّي FK.
مصمّم قاعدة البيانات + DevOps
+500 مخطّط MySQL عبر Stancl tenancy، هجرات لكلّ مستأجر، Redis 7 (4 قواعد: cache/session/queue/broadcast)، Horizon + Supervisor، توجيه nginx 1.26، نسخ mysqldump يوميّة، يُختبَر استرداد النسخ شهريًّا.
مصمّم UI/UX + الهويّة
كلّ شاشة في التطبيقات الستّ. منظومة عربيّة بـTajawal/Readex Pro، فاتح + داكن، جداول بيانات كثيفة بطباعة تحريريّة. الهويّة، الشعار، النصوص التسويقيّة، أنماط UX العربيّة من الصفر.
ضمان جودة + on-call
مصفوفة اختبارات معاديّة من 20 سيناريو (هجوم متعدّد المستأجرين، 10000 push أوفلاين، تذبذب شبكة، انحراف ساعة، توقّف Redis، …). إطلاق canary لكلّ مستأجر. on-call لكلّ حادث على هذه المنصّة طوال 18 شهرًا متواصلة.
الأرقام التي تهمّ.
- +300وحدة محاسبيّةزاتكا، قيد مزدوج، عملات متعدّدة، رواتب، مخزون، تصنيع، مشاريع، CRM، POS — ERP متماسك واحد، لا تكاملات مُلصَقة.
- +2.4Mسجلّ ماليّقيود يوميّة، فواتير، سندات، حركات مخزون، مدفوعات — عبر +500 مستأجر حيّ.
- +500مستأجر في الإنتاجلكلٍّ مخطّط MySQL معزول، مساحة Redis خاصّة، ومجلّد تخزين منفصل.
- <100msمتوسّط زمن استجابة APIالوسيط عبر كلّ نقاط نهاية المستأجرين تحت حمل إنتاج ثابت (آخر 30 يومًا).
- <200msزمن المزامنة p99من بثّ WebSocket إلى استلام Flutter — تحت 1000 كاشير متزامن.
- 99.95%وقت تشغيل API · 12 شهرًاReverb + Horizon + nginx، مراقَب من خارج الشبكة كلّ 60 ثانية.
- 100%POS آمن أوفلاينكلّ إجراء كاشير يُحفَظ محلّيًّا في SQLite ثمّ يتصالح عند إعادة الاتّصال. صفر معاملة مفقودة خلال 18 شهرًا.
- <48 → 1استدعاء API لكلّ pullنقطة Unified Pull واحدة استبدلت 48 نقطة لكلّ كيان. بطّاريّة أقلّ، طيف أقلّ، إقلاع أسرع.
- 0صفّ مكرّر شُحنضمان عدم التكرار من 5 طبقات على كلّ كيان معاملاتيّ. محسوب عبر +2.4M صفّ.
- 78%انخفاض في تذاكر الدعممقاس بعد إطلاق Unified Sync + ضمان عدم التكرار. ما بقي معظمه أسئلة إعداد لا أعطال.
ما الذي كان معطّلاً فعلاً.
الشركات الصغيرة في الخليج كانت تفقد طلباتها عند انقطاع الإنترنت، ثمّ تخسر ساعات أسبوعيًّا في تسوية درج النقد مع سجلّات الخادم في اليوم التالي. أنظمة الـERP السحابيّة الموجودة (Zoho Books، QuickBooks، وافِق) إمّا ترفض العمل أوفلاين، أو تحتفظ بـ«ذاكرة مؤقّتة بلهاء» تُسقط المعاملات بصمت عند إعادة الاتّصال. أنظمة نقاط البيع المكتبيّة المحلّيّة (Foodics على iPad، ETAP، شيتات Excel) تعمل أوفلاين لكنّ لا دفاتر مركزيّة لها — كلّ فرع يصير جزيرة، وسلسلة واحدة في ثلاث مدن لا يمكنها إنتاج قائمة أرباح وخسائر موثوقة. ثمّ هبطت زاتكا «المرحلة الثانية» للفوترة الإلكترونيّة في 2024 وكسرت نصف موّردي نقاط البيع المحلّيّين في ليلة واحدة. المطلوب: بناء أوّل نظام في هذه السوق يكون أوفلاين-أوّلًا، ومتعدّد المستأجرين، وبدرجة محاسبيّة، ومتوافقًا مع زاتكا من البداية — دون تنازل عن أيّ من الأربعة. وشحنه كمنظومة واحدة (موقع تعريفيّ → تسجيل → لوحة إدارة → سوبر-أدمن → نقاط بيع → تطبيق مدير الجوّال → متجر إلكترونيّ للمستأجر → REST API عامّ)، لا خمسة منتجات منفصلة مُلصَقة معًا.
المساحة الصعبة من المشروع.
- 01مزامنة أوفلاين-أوّلًا بحلّ تعارض ثلاثيّ بين تطبيق الكاشير، تطبيق مدير الجوّال، ولوحة الإدارة على شبكات 3G/4G متذبذبة. كلّ عميل يحافظ على مصدر حقيقته المحلّيّ ويتصالح عند إعادة الاتّصال دون تدخّل المشغّل.
- 02اتّساق بين الفروع: تحويل مخزون بدأ في الفرع A يجب ألّا يُحسب مرّتين عند عودة الفرع B بعد سبع ساعات. نُفِّذ عبر version-vector + قفل تفاؤليّ + تمرير تصالح موثوق من الخادم.
- 03تحديثات لحظيّة للمطبخ والمخزون والطلبات عبر WebSockets تتدهور بسلاسة إلى polling لإصدارات Flutter القديمة. تسلسل ثلاثيّ: WebSocket أساسيّ → Unified Pull ثانويّ → polling تقليديّ كأرضيّة.
- 04عزل صارم بين المستأجرين بقاعدة بيانات MySQL مستقلّة لكلّ مستأجر (لا مخطّط مشترك بعمود tenant_id). مستأجر لا يستطيع قراءة صفوف مستأجر آخر تحت أيّ مسار كود — حتى مع تزييف JWT أو سباق طابور خلفيّ أو خطأ مطوّر في استعلام مخصّص.
- 05توافق زاتكا «المرحلة الثانية» للفوترة الإلكترونيّة: تجزئة تشفيريّة للفاتورة بسلسلة فاتورة-سابقة، رمز QR بترميز TLV لبيانات الضريبة، تكامل مع clearance API للـB2B، تكامل مع reporting API للـB2C — كلّ ذلك ثنائيّ اللغة (عربيّة أوّلًا).
- 06تزامن عالٍ: حتى 1500 جلسة كاشير متزامنة في ذروة الغداء عبر كلّ المستأجرين. كاتب-واحد لكلّ مستأجر يعني أنّ كلّ مستأجر يجب أن يتعامل مع ~10 كتّاب متزامنين دون قفلات تسلسليّة.
- 07توليد أرقام مستندات من جهة الخادم يُنتج أرقام فواتير مستقرّة حتى لو دفعت 50 جهازًا أوفلاين تراكمها في اللحظة ذاتها. مجمع حجز لكلّ سلسلة لكلّ جهاز بـTTL لتحرير الفشل.
- 08توافق رجعيّ دائم — إصدارات Flutter القديمة في الميدان يجب أن تستمرّ بالعمل مع تطوّر الخادم. كلّ نقطة API مُصدَّرة، والإصدارات القديمة تعيش 18 شهرًا على الأقلّ بعد الإلغاء.
- 09تجهيز مستأجر جديد في أقلّ من 30 ثانية: من نقرة «سجّل» إلى «أصدر أوّل فاتورة» يجب أن يشمل إنشاء قاعدة MySQL، تشغيل 80+ migration، تجهيز شجرة حسابات افتراضيّة (إنكليزيّ + عربيّ)، إعداد ضريبة، عملاء تجريبيّين، أدوار مستخدمين، قوالب افتراضيّة، وتوجيه subdomain المتجر.
- 10محرّك اشتراك ببروراتا منتصف-دورة: ترقية من Starter إلى Pro في اليوم 17 يجب أن يرصد 13 يوم Starter غير مُستخدَم ويحسب 13 يوم Pro بنسبة — وينتج فاتورة ضريبيّة تنجو من تدقيق المحاسب.
- 11هجرات schema لـ Drift (SQLite محلّيّ في Flutter) على هاتف قد يكون 30 إصدارًا متأخّرًا. الهجرات يجب أن تكون تدريجيّة، idempotent، تتعافى من تطبيق جزئيّ، ولا تُسقط بيانات مستخدم بصمت أبدًا.
- 12تكامل أجهزة نقاط البيع على Android: طابعات إيصالات USB (ESC/POS)، فتح أدراج النقد عبر USB، قارئات باركود USB (وضعَا HID + serial)، موازين Bluetooth، طرفيّات بطاقات (iZettle/Geidea).
- 13إبطال كاش متعدّد المستأجرين: حين يعدّل مستأجر منتجًا، صفحة Next.js للمتجر (ISR لكلّ مستأجر) يجب أن تُبطَل خلال ثوانٍ، لكنّ كاش مستأجر شقيق يجب أن يبقى سليمًا.
- 14تبديل محرّك الضريبة الحيّ حين تتغيّر قواعد السعوديّة والإمارات والبحرين — دون إعادة نشر الـAPI أو كسر الفواتير التاريخيّة.
انتصارات هندسيّة تستحقّ التسمية.
محرّك مزامنة أوفلاين مع إعادة كتابة المفاتيح الأجنبيّة
كاشير ينشئ عميلًا جديدًا أثناء فاتورة وهو أوفلاين. صفّ العميل ينال UUID محلّيّ، الفاتورة تشير إلى الـUUID، والمدفوعات وقيود اليوميّة تشير إلى UUID الفاتورة. عند إعادة الاتّصال، محرّك دفع مُرتَّب طوبولوجيًّا يمشي الرسم البيانيّ للتبعيّة: العميل أوّلًا، يلتقط معرّف الخادم، يعيد كتابة كلّ مرجع تابع (قائمة بيضاء بـ14 عمود FK تشمل contact_id, supplier_id, account_id, product_id, order_id, voucher_id) قبل دفعها. كلّ صفّ يحمل مفتاح idempotency مُعكَس على الخادم؛ دفعة مُقرَّة جزئيًّا لا تتكرّر أبدًا عند إعادة المحاولة. ألغى صنف «العميل أوفلاين لم يُزامن» دون استعلام واحد يدويّ لإزالة التكرار.
سباقات المخزون بين الفروع
كاشيران في فرعين يبيعان آخر وحدة في الوقت ذاته. القفل التفاؤليّ: صفّ المخزون يحمل عمود إصدار، حمولة الدفع تحمل الإصدار الذي بُنيت عليه، الخادم يرفض الكتابة إذا تحرّك الإصدار. التطبيق يُظهر حوار «المخزون تغيّر — أكّد» بدل البيع الزائد الصامت. مع منطق الكميّات المحجوزة في نافذة المعاملة المفتوحة وcron تصالح ليليّ، حوادث البيع الزائد اقتربت من الصفر عبر 500+ مستأجر.
أرقام فواتير مستقرّة عبر الدفعات الأوفلاين (بدرجة ZATCA)
ZATCA تشترط أرقام فواتير صاعدة بلا فجوات وكلّ فاتورة مرتبطة تشفيريًّا بالسابقة. الأجهزة الأوفلاين تدفع خارج الترتيب. نقلنا تخصيص الأرقام إلى خدمة NumberPool على الخادم تحجز رقمًا لكلّ جهاز لكلّ سلسلة بـTTL، تثبّت عند أوّل دفع ناجح، تحرّر عند انتهاء TTL لو فشل الجهاز. الهاش المُسَلسَل يُحسب على الخادم بعد تثبيت الرقم، فالسلسلة تبقى سليمة حتى لو ترتيب الجهاز خاطئ. صفر فجوات، صفر تكرار، صفر شكاوى في 14 شهرًا من امتثال ZATCA «المرحلة الثانية».
ضمان عدم التكرار من خمس طبقات (Five-Layer Zero-Duplicate)
كلّ جدول معاملاتيّ محميّ بخمس طبقات مستقلّة، فالصفّ المكرّر يتطلّب فشل الخمسة معًا. L1: فهرس UNIQUE جزئيّ على server_id (ضمان فيزيائيّ على مستوى DB). L2: سلامة الدفع — الصفوف اليتيمة تُعيد المحاولة تحت نفس مفتاح idempotency، لا تنجح صامتةً. L3: idempotency السحب — إعادة السحب UPDATE للصفوف الموجودة، لا INSERT أبدًا. L4: استقرار المفتاح التجاريّ — رقم الفاتورة أوفلاين = رقم الفاتورة أونلاين للأبد. L5: ماسح تباين ليليّ بعد الكتابة يضع علامة على أيّ صفّ هاشه يختلف بين المحلّيّ والخادم. احتمال تكرار مهمل رياضيًّا، أثبتته عبر 2.3 مليون صفّ مُزامَن.
السحب الموحَّد بدل 48 نقطة مزامنة منفصلة
النسخة الأولى كانت تجري 48 نداء API منفصلًا في كلّ مزامنة بداية باردة (واحد لكلّ كيان: منتجات، فئات، وحدات، ضرائب، عملاء، موردون، حسابات، طلبات، سندات، رواتب، …)، كلّ واحد بحمل مصادقة + خنق + handshake TLS. معظمها كان فارغًا. استبدلناها بنداء POST /sync/pull واحد يأخذ خريطة مؤشّر لكلّ كيان ويعيد ردًّا مُتدفّقًا لكلّ كيان فيه الفرق + الشواهد. البداية الباردة هبطت من 4.8 ثانية إلى 0.9 ثانية على 4G؛ Cloudflare WAF توقّف عن وسم الانفجار كمشبوه؛ استهلاك بيانات الجوّال هبط ~70٪ لكلّ كاشير يوميًّا.
تجهيز DB لكلّ مستأجر في أقلّ من 30 ثانية
مسار التسجيل: إنشاء قاعدة MySQL، تعيين مستخدم DB لكلّ مستأجر بصلاحيّات محدودة، تشغيل 80+ migration بشكل idempotent (كلّ واحدة ملفوفة بـtry-catch فإعادة التشغيل على فشل جزئيّ آمنة)، تجهيز شجرة حسابات افتراضيّة (12 جذر × لغتين × متغيّرات السعوديّة/الإمارات/البحرين)، تجهيز رموز ضريبة، تجهيز أدوار + مستخدم admin، تجهيز قوالب فاتورة/إيصال/عرض سعر افتراضيّة، توجيه subdomain المستأجر في المتجر، إرسال بريد ترحيب. كلّها سلسلة وظائف Horizon بـrollback لكلّ خطوة. من البداية للنهاية: أقلّ من 30 ثانية. كلّ خطوة قابلة للاستئناف.
محرّك فوترة اشتراك ببروراتا منتصف-دورة
ترقية من Starter ($29/شهر) إلى Pro ($79/شهر) في اليوم 17 من دورة 30 يومًا يجب أن يرصد 13 يوم Starter غير مُستخدَم ($12.57) ويحسب 13 يوم Pro ($34.23)، ويُنتج فاتورة ضريبيّة واحدة تنجو من تدقيق ZATCA. بنيت محرّك بروراتا قائم على Decimal فوق Stripe + بوّابات محلّيّة، يتعامل مع: تغييرات خطّة منتصف-دورة (صعودًا أو هبوطًا)، إضافات مقاعد، تفعيل إضافات (فروع إضافيّة، مستودعات إضافيّة)، إعادة محاولات دفع مع backoff، رسائل dunning، تخفيض رشيق عند الفشل. الضريبة تُحسب على المبلغ المُجزَّأ لكلّ نطاق، ثمّ تُقسَّم بين الفاتورة الأصليّة وإشعار دائن حين تُنشئ البروراتا رفضًا.
هجرات schema Drift على هواتف 30 إصدارًا متأخّرة
كاشير يحدّث التطبيق بعد 8 أشهر — DB المحلّيّ في schema نسخة 12، التطبيق يتوقّع نسخة 42. كتبت محرّك هجرات يمشي كلّ نسخة وسيطة بالترتيب، كلّ هجرة idempotent وآمنة من الانهيار (تستخدم أنماط ALTER + COALESCE، لا تستخدم DROP أبدًا)، تسجّل التقدّم في جدول meta فانقطاع كهرباء في منتصف الهجرة يستأنف من آخر خطوة مُثبَّتة. اختُبر عبر 30+ قفزة اصطناعيّة لضمان عدم وجود مسار في رسم النسخ يُسقط بيانات بصمت.
تكامل أجهزة نقاط البيع على تابلتات Android
طابعات إيصالات USB (ESC/POS) — 3 موّردين، كلّ منهم بشذوذاته؛ طابور الطباعة هو isolate خاصّ مُدار بـRiverpod ينجو من إعادة تشغيل Activity. فتح أدراج النقد عبر USB — يُطلَق بسلسلة escape للطابعة. قارئات باركود USB — كشف تلقائيّ لوضع HID مقابل serial. موازين Bluetooth — مقترنة مرّة، يُستطلَع عليها isolate يعيد الاتّصال عند تذبذب Bluetooth. طرفيّات بطاقات (Geidea، Foodics-مجمَّع، iZettle) — مُدمَجة عبر SDK البائع، مع وضع فولباك «المبلغ يدويًّا» حين تكون الطرفيّة أوفلاين. كلّ أحداث الأجهزة تمرّ عبر طبقة OrderBus event sourcing واحدة فحالة فشل أجهزة لا تحجب فاتورة مفتوحة.
إبطال كاش لكلّ مستأجر عبر الستاك
حين يعدّل المستأجر X المنتج P: (أ) كاش Laravel للمستأجر X يُبطل مفتاح قائمة المنتجات، (ب) متجر Next.js للمستأجر X يُطلق إعادة تحقّق ISR لـ/products و/products/P، (ج) تطبيق نقاط البيع للمستأجر X يستلم بثّ Reverb ويُحدّث صفّ Drift المحلّيّ. كاشات المستأجر Y تبقى سليمة. نُفِّذ بوسوم كاش مُسَمّاة بـtenant_id؛ المتجر يستخدم API إعادة التحقّق عند الطلب في Next.js بسرّ لكلّ مستأجر لمنع تسميم الكاش عبر المستأجرين.
إطلاق WebSocket تحت مفتاح ميزة لكلّ مستأجر
لم نستطع شحن Reverb لكلّ المستأجرين دفعة واحدة — مخاطر عالية. boolean `websocket_enabled` لكلّ مستأجر يتحكّم بإطلاق المراقبين. عملاء Flutter القدامى يستمرّون بالـpolling (أرضيّة الفولباك). الجدد يستقبلون التحديثات اللحظيّة لحظة قلب المفتاح. المفتاح قابل للقلب من السوبر-أدمن من خليّة واحدة — تراجع جزئيّ يستغرق ثانية، لا إعادة نشر. التسلسل الثلاثيّ (WebSocket → Unified Pull → polling تقليديّ) يعني تدهور Reverb إلى مزامنة 60 ثانية، لا تطبيق ميت.
مصادقة SSO عبر الأنظمة
مستخدم يسجّل دخول إلى admin.X.mohaseb.co ثمّ ينقر «افتح متجري» — يهبط في متجر Next.js كـadmin مصادَق (فيرى المنتجات غير المنشورة). بنيت رمز تسليم مُوقَّع: Laravel يُصدِر JWT قصير العمر بنطاق «معاينة متجر»، يُحوِّل إلى Next.js، Next.js يتحقّق من التوقيع، يُبادل بـcookie جلسة بنطاق ذلك الـsubdomain. نفس النمط يعالج تبديل كاشير → مدير على نفس التابلت، وسوبر-أدمن → انتحال هويّة المستأجر (مع قيد سجلّ تدقيق عند كلّ انتحال).
كيف يتشابك النظام.
طبقة الحافّة
طبقة العملاء (4 ويب · 2 موبايل · API عامّ)
طبقة التطبيق (Laravel 11 موحّد — واعٍ بالمستأجر)
طبقة اللاتزامن واللحظيّ
طبقة البيانات
المراقبة والسلامة
القرارات وراء كلّ اختيار.
Laravel 11 (الباك-إند)
- منظومة متعدّدة المستأجرين ناضجة (Stancl/tenancy لـDB لكلّ مستأجر، Spatie/permission للـRBAC، Spatie/medialibrary لرفع المستأجرين) — كلّها إنتاجيّة بدعم نشط.
- دعم WebSocket من الدرجة الأولى عبر Reverb في Laravel 11 — لا خدمة Node إضافيّة نعتني بها؛ سياق المصادقة موحَّد طرفًا لطرف.
- global scopes في Eloquent تجعل قواعد سجلّ التدقيق + الحذف الناعم + عزل المستأجرين سهلة الفرض على طبقة الـModel، لا منتشرة عبر الاستعلامات.
- Queue + Horizon + Mail + Notifications أصليّة في الإطار، لا غراء طرف ثالث — كلّ اهتمام عرضيّ يستخدم نفس الاصطلاحات.
- Job batching + chains تعبّر عن مسار «تجهيز مستأجر» طبيعيًّا: إنشاء DB → migrate → seed → notify، مع تراجع عند أيّ فشل.
Livewire + Alpine.js (لوحات الإدارة)
- مُولَّد من الخادم بتحديثات تفاعليّة — لا خطّ بناء SPA منفصل، لا تكرار API للنماذج، لا عبء إدارة حالة من جهة العميل.
- مطوّر واحد يستطيع شحن ميزة كاملة (model + migration + Livewire component + Blade view) في PR واحد دون تبديل سياق.
- تحقّق النموذج، رفع الملفّات، فرز الجداول كلّها مجّانًا مع Livewire — ما هو 300 سطر React + axios يصبح 40 سطر Blade.
- يُرسَم بسرعة على حواسيب المتاجر الرخيصة (لا تكلفة hydration لإطار JS).
Next.js 14 (متاجر المستأجرين)
- ISR لكلّ مستأجر يعطي صفحات المتجر TTFB أقلّ من 200 مللي ثانية دون إعادة بناء عند كلّ تعديل منتج — التحقّق يجري في الخلفيّة.
- App Router middleware يقرأ subdomain المستأجر مرّة واحدة ويحقن السياق في كلّ صفحة — لا مكرّرات لكلّ مسار.
- تحسين الصور، تقسيم كود تلقائيّ، دعم edge runtime — ميزات كنت سأعيد بناءها يدويًّا.
- SEO مهمّ للمتاجر: SSR + structured data + توليد sitemap كلّها مدمجة.
Flutter + Drift + Riverpod
- كود Dart واحد يشحن Android (أساسيّ)، iOS، macOS، Windows بنفس منطق الأعمال. مشغّلات أجهزة نقاط البيع في platform channels، الباقي مشترك.
- Drift يعطي استعلامات SQLite مُحدّدة الأنواع وقت الترجمة — كود المزامنة يبقى آمنًا عبر تطوّر المخطّط؛ محلّل Dart يلتقط انحراف الأعمدة وقت البناء.
- family providers + auto-dispose في Riverpod يعالجان نمط «10 جلسات كاشير مفتوحة في الوقت ذاته، لكلّ منها حالة خاصّة» بسلاسة.
- Hot reload على ماكنة كاشير فعليّة عبر USB يجعل إعادة تخطيط الإيصالات سريعة.
- مُختبَر حتى «طابعة ترجع بايتات قمامة على بطاريّة 14٪» — نموذج isolate في Flutter يسمح بانهيار وإعادة تشغيل طابور الطباعة دون إسقاط الفاتورة المفتوحة.
MySQL 8 (DB لكلّ مستأجر)
- أعمدة JSON تبسّط الحقول متعدّدة اللغات دون التنازل عن سلامة العلاقات — ابحث في JSON، اربط بمفتاح أجنبيّ على الصفّ.
- CTEs ودوال النوافذ تعالج استعلامات إغلاق الفترة المحاسبيّة (الميزان التجريبيّ، الأرباح والخسائر، الميزانيّة العموميّة) بأناقة.
- عزل قاعدة بيانات لكلّ مستأجر يعني أنّ مستأجرًا واحدًا صاخبًا لا يستطيع التأثير على الآخرين — وتصدير بيانات مستأجر هو مجرّد mysqldump لقاعدة واحدة.
- أدوات نسخ احتياطيّ ناضجة، تكرار مفهوم جيّدًا، كلّ مهندس عمليّات في المنطقة يستطيع قراءته.
Stancl/Tenancy
- تبديل المستأجر الحاليّ هو شأن middleware — كلّ استعلام Eloquent داخل طلب يتحدّث تلقائيًّا مع القاعدة الصحيحة.
- دورة حياة المستأجر المدمجة (إنشاء / حذف / حذف ناعم بفترة سماح) توفّر أسابيع من العمل المخصّص.
- فضاء Redis لكلّ مستأجر، وسم طابور، prefix تخزين، prefix مفتاح كاش — كلّها مفروضة من الحزمة، لا تحتاج انضباط مطوّر.
Redis + Horizon
- ضغط طابور موثوق في زحمة الظهيرة حين تدفع 1500 كاشير في الدقيقة ذاتها.
- واجهة Horizon تكشف الوظائف العالقة ومقاييس كلّ طابور دون كتابة لوحة مخصّصة.
- Pub/Sub هو النقل الطبيعيّ لبثّ Reverb — نفس Redis، لا قطعة متحرّكة إضافيّة.
- تجزئة الطوابير لكلّ مستأجر تمنع إعادة محاولات webhook فوترة لمستأجر صاخب من تجويع مزامنة كاشير مستأجر آخر.
Reverb (WebSocket)
- داخل عمليّة Laravel ذاتها — السياق نفسه للمصادقة، لا تبادل رمز منفصل، لا خدمة Node منفصلة للمراقبة.
- بروتوكول سلكيّ متوافق مع Pusher — عملاء Flutter القدامى عبر pusher-channels-dart استمرّوا دون أيّ تعديل بعد الانتقال.
- فضاءات قنوات لكلّ مستأجر تجعل التحكّم بالوصول تصريحيًّا: محاولة subscribe لـtenant.X.orders من جلسة مصادَقة لـtenant.Y تعيد 403 على طبقة القناة.
- قابل للتوسّع أفقيًّا خلف Redis adapter حين يحتاج مستأجر واحد عدّة نسخ.
Nginx + Supervisor (بدل Docker)
- ثنائيّ مُجرَّب على Debian — لا عبء Docker على VPS صغير ندفع فيه ثمن المعالج الخام.
- سياسات إعادة تشغيل Supervisor تعالج تعطّل Reverb / Horizon خلال ثوانٍ، مع خنق فلا يستهلك حلقة الانهيار الجهاز.
- ملفّ Nginx واحد لكلّ subdomain — سهل التدقيق، سهل grep، سهل التراجع.
- لا تعقيد container orchestration لنظام قصّة نشره «git pull + composer install + supervisor reload».
كيف تبقى المنصّة جديرة بالثقة.
- مصادقة JWT بـ15 دقيقة access و30 يومًا refresh، تدوير refresh + كشف إعادة الاستخدام — رمز مسروق يُطلق مسح شجرة-الجلسة-كاملة ويفرض إعادة دخول على كلّ الأجهزة.
- RBAC بأكثر من 50 صلاحيّة تُجمَّع في أدوار؛ تجاوز ADMIN صريح ومُدقَّق؛ انتحال السوبر-أدمن يُسجِّل قيد تدقيق عند الدخول وعند كلّ إجراء أثناء الانتحال.
- تجزئة Argon2id لكلمات المرور (PHP password_hash بتكلفة صريحة) — لا إرث MD5/SHA، لا اختصار PBKDF2. سياسات كلمة المرور مفروضة على الخادم لا على العميل.
- عزل صارم بين المستأجرين على طبقة اتّصال قاعدة البيانات (كلّ طلب يبدّل اعتمادات DB إلى مستخدم محدود نطاق المستأجر) + شبكة أمان global-scope في Eloquent. محاكاة هجمات عبر المستأجرين على كلّ نشر.
- مستخدم DB لكلّ مستأجر له GRANTs محدودة (SELECT/INSERT/UPDATE/DELETE على قاعدته فقط — لا information_schema، لا قواعد أخرى، لا صلاحيّة FILE).
- سجلّ تدقيق على كلّ تعديل (من، ماذا، JSON قبل/بعد، أين، متى، IP، UA، request_id) — قابل للاستعلام من إدارة المستأجر، غير قابل للتعديل، محفوظ 7 سنوات للامتثال.
- تحديد معدّل لكلّ مسار عبر Laravel Throttler — نقاط النهاية العامّة 60/دقيقة/IP، نماذج التواصل 5/دقيقة/IP، تسجيل الدخول 5/دقيقة/IP+اسم مستخدم، إعادة تعيين كلمة المرور 3/ساعة/بريد.
- مصادقة WebSocket توقّع كلّ اشتراك قناة بمعرّف المستأجر + معرّف المستخدم؛ محاولات عبر المستأجرين تعيد 403، تُسجَّل، تُنبَّه عند عتبة 10/دقيقة.
- رموز CSRF على كلّ نموذج مغيِّر للحالة؛ نمط double-submit cookie + same-site=lax على cookies الجلسة.
- mysqldump يوميّ لكلّ مستأجر + gzip باحتفاظ 30 يومًا، dump كامل أسبوعيّ للـlandlord، تدوير شهريّ خارج الموقع، تجارب استعادة فصليّة (مؤقَّتة، موثَّقة).
- تشفير الأعمدة الحسّاسة (كلمات SMTP، أسرار OAuth، مفاتيح Stripe API، شهادات ZATCA) على طبقة التطبيق باستخدام مُشفِّر Laravel المدمج بمفاتيح لكلّ مستأجر.
- توقيع فواتير ZATCA تشفيريًّا بشهادات لكلّ مستأجر صادرة عبر بوّابة ZATCA، مع تحقّق سلسلة-الثقة على كلّ فاتورة.
- تحقّق رفع الملفّات: استشعار MIME (لا الثقة بالامتداد)، فحص magic-byte، حدّ حجم لكلّ نوع، hook فحص فيروسات، عزل S3-prefix لكلّ مستأجر.
- ترويسات CSP + Strict-Transport-Security + X-Content-Type-Options + X-Frame-Options + Referrer-Policy + Permissions-Policy على كلّ ردّ عبر Nginx.
- توقيعات Webhook مُتحقَّق منها على كلّ وارد (Stripe HMAC، ZATCA RSA) — لا webhook idempotent يُعالَج مرّتين (مفتاح idempotency + حماية إعادة تشغيل 30 يومًا).
بُنيت للنموّ، لا لمجرّد الإطلاق.
مصمَّم للتوسّع الأفقيّ على طبقة الـAPI بلا-حالة (عمّال Laravel خلف Nginx، لا session affinity)، مع كاتب MySQL أساسيّ واحد لكلّ مستأجر. عنق الكاتب الواحد مقصود — سلامة المحاسبة تتفوّق على الإنتاجيّة الخامّة؛ محاسب يرى رسومًا مزدوجة يفقد الثقة دائمًا. التوسّع يأتي بـsharding المستأجرين عبر نسخ كاتبة متعدّدة (مُنفَّذ مسبقًا عبر Stancl multi-DB لكنّ غير مطلوب حملًا بعد)، لا بـmulti-master يهدّم سلسلة الفاتورة التشفيريّة لـZATCA. طبقة المتجر تتوسّع باستقلال على edge ISR بنمط Vercel مع وسوم كاش لكلّ مستأجر. Reverb يتوسّع أفقيًّا خلف Redis pub/sub حين تحتاج حمولة وقت حقيقيّ لمستأجر واحد أكثر من نسخة Reverb واحدة.
- دول متعدّدة — السعوديّة، الإمارات، البحرين (محرّكات VAT منفصلة)
- نشر متعدّد العلامات / متعدّد الفروع تحت مستأجر واحد
- توسّع API أفقيّ (عمّال بلا حالة خلف Nginx)
- shard MySQL لكلّ مستأجر للجار الصاخب
- فضاء Redis لكلّ مستأجر لعزل الكاش + الطابور
- فضاء قنوات Reverb لكلّ مستأجر (عزل عبر-المستأجرين مفروض)
- أحمال معاملاتيّة عالية الانفجار (1500+ كاشير متزامن، p99 <200 مللي ثانية)
- كاش edge للمتجر مع وسوم إبطال لكلّ مستأجر
- حدود معدّل لكلّ مستأجر تمنع سوء استخدام API لمتجر من التأثير على الآخرين
- تجزئة طابور لكلّ مستأجر تمنع عواصف webhook الفوترة من تجويع مزامنة نقاط البيع
- جلسة كاشير أوفلاين تصل إلى 24 ساعة قبل فرض إعادة مزامنة (قابلة للتعديل لكلّ مستأجر)
قبل وبعد.
قبل
- فقدان الطلبات في انقطاع الشبكة — لا سجلّ ولا استعادة.
- تسوية يدويّة بين درج النقد والدفاتر كلّ صباح.
- قوائم الأرباح والخسائر لكلّ فرع تُحسب في Excel أوفلاين.
- الإقرار الضريبيّ كان عمليّة أربعة أيّام لكلّ ربع.
- لا رؤية لحظيّة للمخزون بين الفروع → بيع زائد متكرّر.
- 48 نداء مزامنة منفصل عند البداية الباردة، ~4.8 ثانية على 4G.
بعد
- صفر طلب مفقود — تخزين محلّيّ + مزامنة idempotent.
- تسوية آليّة؛ اجتماع الصباح يقرأ تقريرًا واحدًا.
- قائمة أرباح وخسائر موحَّدة لحظيًّا عبر كلّ الفروع.
- الإقرار الضريبيّ يصدّر XML متوافقًا مع ZATCA بنقرة.
- رؤية المخزون بين الفروع تحت 200 مللي ثانية في p99.
- نداء سحب موحَّد واحد عند البداية الباردة، ~0.9 ثانية — أسرع 5 أضعاف.
ما علّمنا إيّاه هذا البناء.
- تعقيد مزامنة الأوفلاين دائمًا مُستهان به بمعامل 3. خطّط ثلاثة أضعاف الوقت الذي تظنّه، واكتب ماسح التباين قبل محرّك المزامنة لا بعده. الماسح هو اختبارك الأمين الوحيد للصحّة.
- مفاتيح الميزة بنطاق مستأجر هي الفرق بين الإطلاق الواثق وحوادث نهاية الأسبوع. اقضِ يومًا في إعداد جدول المفاتيح قبل شحن الميزة خلفه. كلّ تغيير غير تافه يُشحَن خلف مفتاح، ثمّ يُصعَّد، ثمّ يصير افتراضيًّا.
- مفاتيح idempotency على كلّ طبقة (طلب HTTP، حمولة دفع، وظيفة طابور، معالج webhook) ليست بارانويا — هي السبب الوحيد لعدم كون النظام مصنع صفوف مكرّرة. التكلفة عمود إضافيّ في الجدول؛ الفائدة «لن نرسل فاتورة مرّتين أبدًا».
- عزل المستأجرين يجب فرضه على أدنى طبقة قادرة. عزل التطبيق + اعتمادات DB لكلّ مستأجر + GRANTs DB لكلّ مستأجر هم حزام + حمّالة + airbag — لا تكرار. مطوّر مبتدئ ينسى withGlobalScope لا يستطيع تسريب بيانات عبر المستأجرين لأنّ اعتماد الـDB لا يستطيع فيزيائيًّا رؤية الـDB الأخرى.
- التوافق الرجعيّ غير قابل للتفاوض حين لا يمكنك إجبار العميل على التحديث. كلّ نقطة API تعيش 18 شهرًا بعد الإلغاء. إصدارات Flutter القديمة في متاجر نائية تستمرّ بالعمل لأنّ الخادم يعامل كلّ إصدار مواطن من الدرجة الأولى.
- WebSockets بلا polling فولباك هشاشة ستندم عليها. التسلسل الثلاثيّ (WebSocket → سحب موحَّد → polling تقليديّ) يعني أنّ تعطّل Reverb يتدهور إلى مزامنة 60 ثانية، لا إلى تطبيق ميت. الـpolling فولباك هو الأرضيّة، لا السقف.
- ضمان عدم التكرار من خمس طبقات استغرق أسبوعين تصميمًا، ومنع كلّ شكوى صفّ مكرّر في 14 شهرًا إنتاجًا عبر 2.3 مليون صفّ. الرياضيّات أرخص من تذاكر الدعم.
- توليد الأرقام من جهة الخادم (NumberPool) هو الإجابة الصحيحة الوحيدة حين يجب على الأجهزة الأوفلاين إنتاج أرقام صاعدة بلا فجوات. كتابة «INV-{timestamp}» يدويًّا على العميل تبدو ذكيّة حتى أوّل تدقيق ZATCA يفشل لأنّ فاتورة 142 خرجت قبل فاتورة 141.
- DB لكلّ مستأجر يهزم المخطّط المشترك لأيّ نظام يجب أن يعطي المستأجر بياناته. التصدير mysqldump، لا سكربت تصدير 200 سطر بحالات حدود. الحذف DROP DATABASE، لا CASCADE 20-جدول تتمنّى أن ينتهي قبل timeout.
- Livewire للإدارة + Next.js للمتاجر + Flutter للجوّال ليست «ثلاث ستاكات» — هي ثلاث ستاكات لثلاث جماهير. فرض SPA واحد عبرها كلّها كان سيضيف 6 أشهر ويوفّر لا شيء.
- الأجهزة (طابعات، أدراج، قارئات) هي حيث يعيش 30٪ من تذاكر دعم نقاط البيع. لفّ كلّ نداء جهاز بـcircuit breaker؛ اعرض «الطابعة أوفلاين — الفاتورة محفوظة، ستُطبع عند إعادة الاتّصال» للكاشير بدل انهيار الطلب المفتوح.
- العمل منفردًا على المسار الحرج يجبرك على إبقاء التعقيد أمينًا. لو ميزة تتطلّب ثلاثة أيّام من الحمل العقليّ لإمساكها في رأسك، فهي معقّدة أكثر من اللازم — أعد التصميم قبل الشحن. النظام نجا لأنّ لا شيء فيه أذكى مما يلزم.
