Aller au contenu

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/balance lit 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 int64 d'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 :

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

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-full un sens réel.
  • Où vivent les secrets. Tous les secrets sont dans /etc/gerege-wallet/*.env. Le .env de 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/status sur 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