Gerege Wallet¶
Partiel · Couche 4 — Produit vertical ·
Dépôt : wallet-gerege-mn · wallet.gerege.mn · api.wallet.gerege.mn
Le portefeuille numérique du citoyen — un produit où l'on se connecte par eID, où l'on consulte son solde et où l'on effectue des virements par IBAN. Apache Fineract tient le rôle de cœur financier.
Les applications mobiles (iOS SwiftUI, Android Compose) constituent la surface principale ; le web n'est qu'une console d'appoint.
L'architecture de l'argent — la règle la plus importante¶
Fineract est la source de vérité unique pour l'argent. Soldes, opérations et grand livre y résident tous. PostgreSQL ne stocke QUE la correspondance « citoyen ↔ identifiants Fineract », les enregistrements d'idempotence et les préférences utilisateur.
| Couche | Responsabilité |
|---|---|
| Backend Wallet (Go) | Authentification, droits, parcours, audit |
| Apache Fineract 1.15 | Comptes, opérations, soldes, partie double, grand livre |
| PostgreSQL | Correspondance et préférences seulement — jamais de solde ici |
Trois règles en découlent :
- Ne jamais mettre un solde en cache.
/accounts/balancelit directement depuis Fineract : les conditions d'une divergence entre deux systèmes ne surviennent tout simplement pas. - Ne jamais représenter l'argent en flottant. La forme textuelle du JSON est
convertie directement en
int64d'unités mineures (pour le ₮, argent = ₮×100). La voie des erreurs d'arrondi est fermée. - Chaque opération est idempotente. Détail ci-dessous.
Pourquoi un core banking déjà existant ?
Un grand livre financier est un domaine difficile à faire correctement et coûteux à rater : comptabilité en partie double, équilibrage, clôtures, piste d'audit. Fineract a résolu tout cela au fil d'années d'exploitation. Nous n'ajoutons par-dessus que les couches d'identité et de service.
Citoyen ↔ compte ↔ IBAN¶
Chaque citoyen se voit attribuer exactement un client Fineract, un compte
d'épargne et un IBAN. La clé de liaison est le civil_id du citoyen :
L'IBAN mongol compte 20 caractères :
MN | kk | bbbb | aaaaaaaaaaaa
2 | 2 | 4 | 12
│ │ │ └─ numéro de compte (Fineract savings ID, complété de zéros)
│ │ └──────── code de banque/établissement (4 chiffres)
│ └────────────── clé de contrôle mod-97
└─────────────────── code pays
Les 12 chiffres du compte dérivent de l'identifiant Fineract : aucune séquence supplémentaire n'est nécessaire et la correspondance inverse est purement arithmétique.
La clé est civil_id, PAS le user_id interne
Dériver l'externalId de Fineract du user_id de PostgreSQL (un UUID neuf à
chaque création de ligne) a été corrigé. Avec ce schéma, reconstruire la base
rompait définitivement le lien citoyen ↔ compte : à la reconnexion, un
nouveau compte et un nouvel IBAN étaient créés, laissant l'ancien solde
orphelin.
Le civil_id est un identifiant à vie (tout utilisateur eID en possède un) ;
une clé qui en dérive ne dépend donc pas de la base. AVANT d'ouvrir un
compte, un client et un compte Fineract existants sont recherchés par cette
clé et réutilisés s'ils existent — de sorte que, même si la correspondance
est perdue, le citoyen retrouve le même IBAN.
Autorisation des virements¶
Un virement est autorisé par la session JWT. L'application appelle
/transfer/iban avec l'access token obtenu à la connexion.
Protection contre les doublons : chaque requête porte un en-tête
Idempotency-Key. Une seconde requête avec la même clé ne crée pas de nouvelle
opération mais renvoie le résultat de la PREMIÈRE — si le réseau coupe et que
l'application renvoie la demande, l'argent ne part pas deux fois. La garantie est
tenue in fine par une contrainte UNIQUE (user_id, idempotency_key) en base.
Le rattachement de la signature a été supprimé
Auparavant, chaque virement était réhaché au format canonique GWT et
confronté à la signature eID PIN2 du citoyen (WYSIWYS — « vous signez ce que
vous voyez »). Cela allait de pair avec des implémentations identiques à
l'octet dans trois portages (Go/Kotlin/Swift) et un contrôle CI sur des
golden fixtures.
Tout cela a été retiré par décision produit. Conséquence : toute partie détenant un access token valide peut faire bouger de l'argent — alors qu'auparavant, même un jeton volé ne permettait pas de virement sans le PIN2.
La connexion par eID demeure ; seule la signature du VIREMENT a été supprimée.
Surface d'API¶
L'authentification provient de la couche socle de Gerege Platform ; les endpoints du portefeuille résident dans ce dépôt.
| Méthode | Chemin | Ce qu'il fait |
|---|---|---|
POST |
/api/v1/auth/initiate |
Envoie un push eID par numéro d'état civil |
GET |
/api/v1/auth/status/{sid} |
Statut + jeton + IBAN (c'est ici que le portefeuille s'ouvre) |
GET |
/api/v1/accounts/balance |
Solde (directement depuis Fineract) |
GET |
/api/v1/accounts/transactions |
Relevé de compte |
GET |
/api/v1/accounts/lookup |
Vérifier l'IBAN d'un bénéficiaire |
POST |
/api/v1/transfer/iban |
Virement (exige Idempotency-Key) |
GET/DELETE |
/api/v1/beneficiaries |
Bénéficiaires enregistrés |
POST/DELETE |
/api/v1/devices/register |
Enregistrement du jeton push |
POST |
/api/v1/pay/code/initiate |
Jeton QR de paiement à usage unique |
Deux formes de réponse¶
- JSON plat (sans enveloppe) — pour les applications mobiles. Le portefeuille et les endpoints d'authentification mobile utilisent cette forme.
- Une enveloppe
{status, message, data}— pour le BFF web.
En ajoutant un endpoint destiné aux applications, conservez la forme plate. Le BFF web enveloppe la réponse plate dans sa propre enveloppe cliente avant de la transmettre.
Le vocabulaire d'état des applications¶
Les applications ne considèrent comme terminaux que CONFIRMED / REFUSED /
TIMEOUT. Les noms eID internes du backend (COMPLETE/EXPIRED/…) sont
projetés vers l'extérieur en un seul endroit.
Ce sont les applications qui définissent le contrat
Les applications iOS et Android ont été construites AVANT le backend et attendent les formes ci-dessus. En cas de désaccord entre l'application et le backend, c'est le backend qui est corrigé.
Applications mobiles¶
| iOS | Android | |
|---|---|---|
| Technologie | SwiftUI | Kotlin + Compose |
| Connexion | ✅ | ✅ |
| Solde / relevé | ✅ | ✅ |
| Virement | ✅ | ⏳ écran pas encore réalisé |
| Paiement par QR (EMVCo) | ✅ | ⏳ |
| Enregistrement push | ✅ | ✅ |
Les applications n'atteignent jamais directement le domaine eID : tout le
trafic passe par api.wallet.gerege.mn. Pour se connecter, le citoyen saisit son
PIN en réponse à un push reçu dans l'application eID.
Sécurité¶
- Row-Level Security. L'API se connecte à la base avec un rôle qui n'est PAS
superuser (un garde-fou le vérifie en production), de sorte que les policies
RLS s'appliquent réellement. Chaque table « par utilisateur » a sa propre
policy. Les écritures serveur de confiance — ouverture de compte, création
d'un enregistrement de virement — sont effectuées à part sous un rôle
service; le citoyen n'a aucun droit d'écriture sur ces tables. - TLS pour la base. PostgreSQL dispose de TLS via une CA privée, ce qui donne
au
sslmode=verify-fullun sens réel. - Où vivent les secrets. Tous les secrets sont dans
/etc/gerege-wallet/*.env. Le.envde l'application est RÉGÉNÉRÉ à chaque déploiement : une modification à la main sera donc effacée au déploiement suivant. - Rate limits.
/auth/*~5 requêtes/min (corps plafonné à 4 KiB), le long-poll/auth/statussur sa propre limite plus souple, et les endpoints qui déplacent de l'argent ~30/min. - Idempotence. Chaque virement exige un
Idempotency-Key— si le réseau coupe et que l'application renvoie la demande, l'argent ne part pas deux fois. Le rattachement de signature ayant disparu, c'est la protection PRINCIPALE contre les doublons. - Isolation de Fineract. Il n'écoute que sur
127.0.0.1:8090— aucun chemin depuis l'extérieur.
Déploiement¶
PAS Docker — systemd natif :
gerege-wallet.slice
├── gerege-wallet-fineract.service # Fineract 1.15 (JAR, 127.0.0.1:8090)
├── gerege-wallet-api.service # API Go (127.0.0.1:8080)
└── gerege-wallet-web.service # BFF Next.js (127.0.0.1:3000)
PostgreSQL, Redis et nginx sont des services de l'hôte. nginx expose
wallet.gerege.mn et api.wallet.gerege.mn en TLS Let's Encrypt.
CD : après une fusion dans main, le workflow Deploy démarre dès que la CI
est verte. Toute la construction a lieu sur le runner et seul le résultat atteint
le serveur — aucun toolchain Go/Node n'y est nécessaire et la fenêtre de
perturbation est courte. Un déploiement n'est enregistré comme réussi qu'après
avoir vérifié non seulement /health, mais qu'un chemin protégé répond
correctement.
État actuel¶
| Capacité | État |
|---|---|
| Connexion eID (avec identifiants RP) | Opérationnel |
| Ouverture automatique du portefeuille + attribution d'IBAN | Opérationnel |
| Solde / relevé | Opérationnel |
| Bonus de bienvenue pour les nouveaux portefeuilles | Opérationnel |
| Virement IBAN (autorisé par JWT) | Implémenté, peu testé |
| Virement / QR sur Android | Prévu |
| App Store / TestFlight | En préparation |
Le code banque de l'IBAN est provisoire
Le code de banque/établissement actuel est une valeur temporaire, en attente d'un code réel délivré par la Banque de Mongolie. Un IBAN attribué à un citoyen ne doit pas changer : cette valeur ne sera donc pas modifiée une fois de vrais utilisateurs en place.
Documentation détaillée¶
Les documents ARCHITECTURE, DEVELOPMENT, API_CONTRACT et SECURITY se
trouvent dans le répertoire backend/docs/ du dépôt wallet-gerege-mn (par
paires EN/MN). Les instructions de build mobile sont dans ios/README.md, celles
de déploiement dans docs/DEPLOYMENT.md.
Plateformes liées : eID Mongolia · G-Sign · Gerege Platform · Gerege Verify