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