انتقل إلى المحتوى

Gerege Wallet

جزئي · الطبقة 4 — منتج قطاعي · المستودع: wallet-gerege-mn · wallet.gerege.mn · api.wallet.gerege.mn

محفظة المواطن الرقمية — منتجٌ يدخل إليه المستخدم بـ eID، فيرى رصيده ويُجري تحويلات بـ IBAN. ويعمل Apache Fineract نواةً مالية له.

وتطبيقات الهاتف (iOS بـ SwiftUI، وAndroid بـ Compose) هي السطح الأساسي؛ أمّا الويب فوحدة تحكّم مساندة.

معمارية المال — أهمّ قاعدة

Fineract هو المصدر الوحيد للحقيقة في شأن المال. فالأرصدة والحركات ودفتر الحسابات كلّها هناك. ولا تخزّن PostgreSQL إلّا الربطَ بين المواطن ومُعرِّفات Fineract، وسجلّات التكرار الآمن (idempotency)، وتفضيلات المستخدم.

الطبقة المسؤولية
خلفية Wallet (Go) المصادقة والصلاحيات والمسارات والتدقيق
Apache Fineract 1.15 الحسابات والحركات والأرصدة والقيد المزدوج ودفتر الحسابات
PostgreSQL الربط والتفضيلات فقط — ولا يوجد الرصيد هنا أبدًا

وتتفرّع من ذلك ثلاث قواعد:

  • لا تُخزَّن الأرصدة مؤقّتًا. فـ /accounts/balance يقرأ من Fineract مباشرةً — ومن ثمّ لا تنشأ أصلًا ظروفُ تباعدٍ بين نظامَين.
  • لا يُمثَّل المال بعدد عشري عائم. بل تُحوَّل الصيغة النصّية في JSON مباشرةً إلى int64 بالوحدة الصغرى (فبالتوغريك: المال = ₮×100). وبذلك يُسدّ طريق أخطاء التقريب.
  • كل حركة قابلة للتكرار الآمن. والتفصيل أدناه.

لماذا نظام مصرفي جاهز؟

دفتر الحسابات المالية مجالٌ يصعب إتقانه وتغلو كلفة الخطأ فيه: القيد المزدوج، والموازنة، والإقفال، وأثر التدقيق. وقد حلّت Fineract ذلك عبر سنوات من الاستخدام الإنتاجي. ونحن لا نضيف فوقها سوى طبقتي الهوية والخدمة.

المواطن ↔ الحساب ↔ IBAN

لكل مواطن عميلٌ واحد بالضبط في Fineract، وحساب توفير واحد، ورقم IBAN واحد. ومفتاح الربط هو civil_id الخاص بالمواطن:

civil_id ──► externalId = PNOMN-<CIVIL_ID> ──► Fineract client + account
                                              savings account ID
                                                      IBAN

ورقم IBAN المنغولي من 20 محرفًا:

MN | kk | bbbb | aaaaaaaaaaaa
 2 |  2 |    4 |           12
 │    │     │      └─ account number (Fineract savings ID, zero-padded)
 │    │     └──────── bank / institution code (4 digits)
 │    └────────────── mod-97 check digits
 └─────────────────── country code

وخانات الحساب الاثنتا عشرة مشتقّة من مُعرِّف Fineract، ولذلك لا حاجة إلى تسلسلٍ إضافي، والربط العكسي حسابٌ رياضي محض.

المفتاح هو civil_id لا user_id الداخلي

صُحِّح اشتقاقُ externalId في Fineract من user_id في PostgreSQL (وهو UUID جديد مع كل إنشاء صفّ). فبتلك الطريقة كانت إعادة بناء قاعدة البيانات تقطع الربطَ بين المواطن وحسابه قطعًا نهائيًا: فيُنشأ عند الدخول مرّةً أخرى حسابٌ جديد ورقم IBAN جديد، ويبقى الرصيد القديم يتيمًا.

وcivil_id مُعرِّفٌ مدى الحياة (لكل مستخدم eID واحدٌ منه)، فالمفتاح المشتقّ منه لا يعتمد على قاعدة البيانات. ويُبحَث قبل فتح الحساب عن عميلٍ وحسابٍ قائمين في Fineract بهذا المفتاح، فإن وُجدا أُعيد استخدامهما — وبذلك يستعيد المواطن رقم IBAN نفسه حتى لو فُقد الربط.

تفويض التحويل

يُفوَّض التحويل بـجلسة JWT. فيستدعي التطبيق /transfer/iban برمز الوصول الذي حصل عليه عند الدخول.

والحمايةُ من التكرار: يحمل كل طلبٍ ترويسة Idempotency-Key. ولا يُنشئ طلبٌ ثانٍ بالمفتاح نفسه حركةً جديدة، بل يُعيد نتيجة الطلب الأول — فإن انقطعت الشبكة وأعاد التطبيق الإرسال لم يخرج المال مرّتين. ويستند هذا الضمان في نهايته إلى قيد UNIQUE (user_id, idempotency_key) في قاعدة البيانات.

أُزيل ارتباط التوقيع

كان كل تحويل يُعاد تجزئته سابقًا بالصيغة المعيارية GWT ويُقابَل بتوقيع المواطن بـ eID PIN2 (وهو مبدأ WYSIWYS — «توقّع ما ترى»). وكان معه تنفيذٌ متطابق بايتًا ببايت في ثلاثة منافذ (Go وKotlin وSwift)، وفحصٌ في التكامل المستمر على golden fixtures.

وقد أُزيل ذلك كلّه بقرارٍ من جهة المنتج. والنتيجة: أنّ من يملك رمز وصولٍ صالحًا يستطيع تحريك المال — بينما لم يكن ممكنًا من قبل إجراءُ تحويل من دون PIN2 حتى لو سُرق الرمز.

أمّا الدخول بـ eID فباقٍ — ولم يُزَل إلّا توقيع التحويل.

سطح الواجهة البرمجية

تأتي المصادقة من الطبقة الأساسية في Gerege Platform؛ أمّا نقاط نهاية المحفظة فهي في هذا المستودع.

الطريقة المسار ما يفعله
POST /api/v1/auth/initiate يُرسل إشعار eID برقم السجل
GET /api/v1/auth/status/{sid} الحالة + الرمز + IBAN (وهنا تُفتَح المحفظة)
GET /api/v1/accounts/balance الرصيد (من Fineract مباشرةً)
GET /api/v1/accounts/transactions كشف الحساب
GET /api/v1/accounts/lookup التحقّق من IBAN المستفيد
POST /api/v1/transfer/iban التحويل (يستلزم Idempotency-Key)
GET/DELETE /api/v1/beneficiaries المستفيدون المحفوظون
POST/DELETE /api/v1/devices/register تسجيل رمز الإشعارات
POST /api/v1/pay/code/initiate رمز QR للدفع لمرّة واحدة

صيغتان للاستجابة

  • JSON مسطّح (بلا غلاف) — لتطبيقات الهاتف. وتستخدمه المحفظةُ ونقاطُ نهاية المصادقة على الهاتف.
  • غلاف {status, message, data} — لواجهة BFF على الويب.

وعند إضافة نقطة نهاية للتطبيق، التزم الصيغة المسطّحة. أمّا BFF الويب فيضع الاستجابة المسطّحة في غلاف عميله ثم يمرّرها.

مفردات الحالة في التطبيقات

لا تعدّ التطبيقات نهائيًا إلّا CONFIRMED وREFUSED وTIMEOUT. وتُسقَط أسماء eID الداخلية في الخلفية (COMPLETE/EXPIRED/…) إلى الخارج في موضعٍ واحد فقط.

التطبيقات هي التي تُحدّد العقد

فتطبيقا iOS وAndroid بُنيا قبل الخلفية، وهما يتوقّعان الصيغ أعلاه. وإذا اختلف التطبيق والخلفية، صُحِّحت الخلفية.

تطبيقات الهاتف

iOS Android
التقنية SwiftUI ‏Kotlin + Compose
الدخول
الرصيد / الكشف
التحويل ⏳ الشاشة لم تُنجَز بعد
الدفع برمز QR (‏EMVCo)
تسجيل الإشعارات

ولا تصل التطبيقات إلى نطاق eID مباشرةً إطلاقًا — فكل التخاطب يمرّ عبر api.wallet.gerege.mn. وللدخول يُدخل المواطن رقمه السرّي استجابةً لإشعارٍ يصله في تطبيق eID.

الأمان

  • أمن مستوى الصف (RLS). تتّصل الواجهة البرمجية بقاعدة البيانات بدور ليس superuser (ويتحقّق من ذلك حارسُ الإقلاع في الإنتاج)، فتُطبَّق سياسات RLS فعلًا. ولكل جدولٍ خاصٍّ بالمستخدمين سياستُه. أمّا الكتابات الموثوقة من الخادم — كفتح حساب أو إنشاء قيد تحويل — فتُنفَّذ على حدة بدور service؛ ولا يُمنح المواطن حقّ الكتابة في تلك الجداول.
  • TLS لقاعدة البيانات. لدى PostgreSQL تشفير TLS بسلطة تصديق خاصة، ولذلك صار لـ sslmode=verify-full معنى حقيقي.
  • موضع الأسرار. جميع الأسرار في /etc/gerege-wallet/*.env. وملف .env الخاص بالتطبيق يُعاد توليده مع كل نشرة، فتعديله يدويًا يعني أنّ النشرة التالية ستمحو التعديل.
  • تحديد المعدّل./auth/* نحو 5 طلبات/دقيقة (وحدّ الجسم 4 KiB)، و /auth/status بنمط long-poll له حدّه الأوسع الخاص، ونقاط النهاية التي تحرّك المال نحو 30/دقيقة.
  • التكرار الآمن. يستلزم كل تحويل Idempotency-Key — فإن انقطعت الشبكة وأعاد التطبيق الإرسال لم يخرج المال مرّتين. ولمّا زال ارتباط التوقيع، صار هذا هو الآلية الأساسية للحماية من التكرار.
  • عزل Fineract. لا يستمع إلّا على 127.0.0.1:8090 — فلا طريق إليه من الخارج.

النشر

ليس بـ Docker، بل systemd أصلي:

gerege-wallet.slice
├── gerege-wallet-fineract.service   # Fineract 1.15 (JAR, 127.0.0.1:8090)
├── gerege-wallet-api.service        # Go API (127.0.0.1:8080)
└── gerege-wallet-web.service        # Next.js BFF (127.0.0.1:3000)

وPostgreSQL وRedis وnginx خدماتُ المضيف. ويُخرج nginx النطاقين wallet.gerege.mn وapi.wallet.gerege.mn بتشفير TLS من Let's Encrypt.

التسليم المستمر: بعد الدمج في main يعمل سير عمل النشر بمجرّد أن يصير التكامل المستمر أخضر. ويجري البناء كلّه على المُنفِّذ، ولا يصل الخادمَ إلّا الناتج — فلا حاجة إلى أدوات Go/Node هناك، ونافذة التعطّل قصيرة. ولا تُسجَّل النشرة ناجحةً إلّا بعد التحقّق من /health ومن أنّ مسارًا محميًّا يستجيب استجابةً صحيحة.

الحالة الراهنة

الإمكانية الحالة
الدخول بـ eID (ببيانات اعتماد جهة معتمِدة) يعمل
فتح المحفظة تلقائيًا + منح IBAN يعمل
الرصيد / الكشف يعمل
مكافأة ترحيب للمحافظ الجديدة يعمل
التحويل بـ IBAN (بتفويض JWT) مُنفَّذ، والاختبار ناقص
التحويل / QR على Android مخطَّط
App Store / TestFlight قيد الإعداد

رمز المصرف في IBAN مؤقّت

رمز المصرف/الجهة الحالي قيمة مؤقّتة إلى أن يُستحصَل رمزٌ فعلي من مصرف منغوليا. ولا ينبغي أن يتغيّر رقم IBAN الممنوح لمواطن، ولذلك لن تُستبدَل هذه القيمة بعد انضمام مستخدمين حقيقيين.

الوثائق التفصيلية

توجد مستندات ARCHITECTURE وDEVELOPMENT وAPI_CONTRACT وSECURITY في مجلّد backend/docs/ بمستودع wallet-gerege-mn (بأزواج إنجليزية/منغولية). وإرشادات بناء تطبيقات الهاتف في ios/README.md، وإرشادات النشر في docs/DEPLOYMENT.md.

المنصّات ذات الصلة: eID Mongolia · G-Sign · Gerege Platform · Gerege Verify