دفتري (Daftari) — من دفتر محل واحد إلى منصة SaaS متعددة المحلات (Flutter + Firebase Multi-Tenant)

تفاصيل العمل

دفتري تطبيق لإدارة العمليات اليومية لمحلات التجزئة (بدأ بمحل بوظة واحد)، مبني بـFlutter وموزَّع فعلياً على أندرويد، الويب، وويندوز (نفس قاعدة الكود قابلة للبناء على iOS وmacOS ولينكس، لكن ما تم بناؤها أو اختبارها على هاي المنصات لحد الآن). بعد نجاح النسخة الأولى، تطوّر التطبيق ليصبح منصة متعددة المستخدمين ومتعددة المحلات (Multi-Tenant): كل صاحب محل يسجّل حسابه بنفسه ويحصل على بيئة بياناته المعزولة بالكامل عن باقي المحلات، بدل ما يحتاج نسخة كود منفصلة لكل محل. يغطي التطبيق نقطة البيع، حسابات الزبائن والديون، المصاريف، إدارة الموظفين والصلاحيات، العلامة التجارية لكل محل (اسم وشعار)، والتقارير اليومية — بواجهة عربية كاملة (RTL).

المشكلة التي دفعت لبنائه

أصحاب المحلات الصغيرة بيديروا عملياتهم بطريقة يدوية: تسجيل المبيعات والديون على الورق، وفي آخر اليوم لازم حد يقعد بالحاسبة يجمع قديش انباع وقديش الديون المتبقية عالزبائن. هاي الطريقة بطيئة، عرضة للأخطاء، وما بتعطي صورة واضحة وفورية عن الوضع المالي للمحل.

الحل ودوري في المشروع

بنيت التطبيق لحالي بالكامل، من التصميم للنشر، وعلى مرحلتين:

المرحلة الأولى — محل واحد. بنيت نسخة أولى لمحل واحد بمستخدمين متعددين (مدير وموظفين بصلاحيات مختلفة). استخدمت Flutter مع Provider لإدارة الحالة، وشاشة نقطة بيع تسجّل كل عملية بيع بثلاث طرق دفع (كاش، تطبيق دفع، دين) وتربطها بحساب الزبون عند البيع بالدين. الخلفية بالكامل على Firebase: Authentication وCloud Firestore، مع Security Rules أولية لفرض صلاحيات المدير مقابل الموظف.

المرحلة الثانية — تحويلها لمنصة متعددة المحلات. بعد ما نجحت النسخة الأولى، بدل ما أنسخ نفس الكود وأشغّله بشكل منفصل لكل محل جديد، أعدت بناء طبقة البيانات بالكامل لتكون Multi-Tenant. هاي كانت أكبر تحدٍ تقني بالمشروع:

نقلت بيانات المحل من كولكشنات جذرية عامة (/customers, /sales...) إلى بنية معزولة تحت shops/{shopId}/....

أعدت كتابة Firestore Security Rules بالكامل لمنع أي تسريب بيانات بين المحلات، عبر custom claims بالتوكن (token.shops) مع مسار احتياطي من ملف المستخدم بـFirestore لحالات ما قبل مزامنة الصلاحيات.

بنيت آلية هجرة آمنة: وضع "صيانة" (_migration/status) يوقف الكتابة من الكلاينت أثناء نقل البيانات — محمي على مستوى الكود وعلى مستوى Security Rules معاً — وخدمة ShopMembershipRecovery تربط حسابات النظام القديم تلقائياً بالمحل الصحيح بعد الترحيل بدون ما يفقد أي مستخدم قديم وصوله.

أضفت تسجيلاً ذاتياً لمحلات جديدة (Self-Service Registration): صاحب المحل ينشئ حسابه ومحله لحاله مباشرة من التطبيق، عبر مسارين — Cloud Function (registerShop) لما تكون خطة الفوترة تسمح بذلك، أو مسار بديل من طرف العميل (Client Fallback) لتفادي كلفة Cloud Functions بالبداية — مع رجوع تلقائي بين المسارين حسب توفر الخدمة.

أضفت دورة حياة للمحل (تجربة مجانية 15 يوم → مفعّل → موقوف) يتحكم فيها مدير المنصة فقط، بينما صاحب المحل يقدر يعدّل شعاره واسمه الظاهر بس.

كتبت اختباراً آلياً (multishop_isolation_test.mjs على Firestore Emulator) يتحقق من عزل البيانات بين المحلات بتسع سيناريوهات مختلفة (موظف أو مدير محل بيحاول يوصل لبيانات محل تاني) قبل ما أنشر الهجرة على بيانات حقيقية.

كمان بنيت Cloud Function لإرسال تقرير يومي بالإيميل عبر Resend API، ونظام تحديث ذاتي للتطبيق (بما إنه موزَّع كملف APK/EXE مباشر مش عبر متاجر التطبيقات): يتحقق من وجود نسخة جديدة، ينزّلها، وينشرها تدريجياً على نسبة من المستخدمين (staged rollout) عشان أقدر أرصد أي مشكلة قبل ما توصل لكل المستخدمين.

التقنيات المستخدمة

Flutter / Dart — قاعدة كود واحدة، موزَّعة فعلياً على Android وWeb وWindows

Provider — إدارة الحالة (State Management)

Firebase Authentication — تسجيل دخول + Custom Claims لصلاحيات كل مستخدم بكل محل

Cloud Firestore + Security Rules — قاعدة بيانات لحظية، ببنية Multi-Tenant معزولة (shops/{shopId}/...)

Cloud Functions (Node.js v2) — تسجيل محلات جديدة (registerShop)، مزامنة الصلاحيات (syncUserShopClaims)، وإرسال تقارير بالإيميل عبر Resend API

Firebase Storage — رفع شعار كل محل (image_picker + firebase_storage)

نظام تحديث ذاتي — تحقق من إصدار جديد، تنزيل، ونشر تدريجي (staged rollout) باستخدام crypto لتوزيع الأجهزة بشكل ثابت وعشوائي

اختبار آلي بـFirestore Emulator (Node.js) — للتحقق من عزل بيانات المحلات قبل النشر

حزم دعم إضافية: connectivity_plus، permission_handler، package_info_plus، path_provider، open_filex، http، intl

النتيجة

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