Créer une application de caisse QRIS avec Codex : notes sur la création de QRIS-EDC de mars à juillet 2026
Je suis Mohammad Hilmi Bisri, fondateur de PT Hanz Kode Terpadu (HanzCodes) à Jombang. D’après les dates des fichiers du projet, j’ai créé QRIS-EDC de mars à juillet 2026. QRIS-EDC est une application de caisse gratuite qui fonctionne dans le navigateur du téléphone et affiche, pour chaque transaction, un QRIS (le QR code de paiement national indonésien) au montant exact. J’ai écrit la plus grande partie du code avec OpenAI Codex (site externe), un agent de programmation qui, selon OpenAI, s’utilise dans ChatGPT, dans un éditeur de code et dans le terminal.
Le dossier du projet ne conserve aucun journal des conversations avec Codex : ce que je dis ici du rôle de Codex repose donc sur mon propre témoignage. L’ordre des travaux et les détails techniques viennent des dates des fichiers de quatre dossiers de travail et de l’historique git du backend. Si vous tenez une boutique et cherchez comment accepter le QRIS, la comparaison des solutions se trouve dans l’article application de caisse QRIS gratuite.
À l’origine, QRIS-EDC était mon projet pour le PIDI Digdaya X Hackathon 2026 de Bank Indonesia et de l’OJK (l’autorité indonésienne des services financiers), sous le nom de travail Kasir Digdaya. Ce projet n’était pas dans les 80 premiers. Après le hackathon, j’ai continué à le développer, puis je l’ai publié comme application gratuite.
Ordre des travaux
| Période | Travaux réalisés |
|---|---|
| 1er mars 2026 | Première version : une page HTML avec un fichier JavaScript et un fichier CSS. |
| 2 mars 2026 | Découpage de l’application en plusieurs pages : caisse, articles, rapports, scanner QRIS et réglages. |
| Du 4 au 6 mars 2026 | Reconstruction en SPA (application monopage) modulaire. Le dossier engine contient la logique du QRIS, du stock, de l’imprimante et de la synchronisation, le dossier pages contient chaque écran, et le dossier lib contient le stockage et les utilitaires. J’ai d’abord planifié certaines grandes fonctions dans des fichiers Markdown, par exemple les rapports et le consentement à l’envoi de données. |
| Mai 2026 | Backend FastAPI avec une base de données Turso, exécuté sur Vercel. Le dépôt compte 33 commits, tous datés du 14 et du 19 mai. Côté application, j’ai ajouté WebLLM, une bibliothèque qui exécute un petit modèle de langage directement dans le navigateur. |
| Juillet 2026 | Suivi des dettes des clients (kasbon) et visite guidée dans l’application. |
QRIS-EDC se sert de ce modèle de langage pour transformer une commande orale, par exemple « deux cafés, moins sucrés », en contenu du panier. L’utilisateur choisit lui-même le modèle, par exemple une petite version de Qwen ou de Gemma. Le navigateur télécharge les poids du modèle dans son cache à la première utilisation.
Ma façon d’écrire les prompts
En mai 2026, j’ai décrit ma méthode de prompt dans un guide interne :
- Le prompt principal donne à l’agent le rôle de senior pragmatic software engineer (ingénieur logiciel senior et pragmatique), qui donne la priorité à l’exactitude (correctness), à la sécurité, à la facilité de maintenance et à une architecture simple. L’agent n’a pas le droit d’inventer des faits. Si un détail bloque le travail, l’agent pose une seule question courte.
- L’agent traite une tâche, la vérifie, puis passe à la suivante. Je ne demande pas de travail en parallèle et je ne regroupe pas des changements sans rapport entre eux dans une même demande.
- Avant d’écrire du code, l’agent réécrit mon idée brute sous forme de spécification technique : rôle, objectif, contexte, contraintes, fichiers à lire, ordre de travail, méthode de vérification et définition de « terminé ». À cette étape, l’agent retire aussi le langage promotionnel, les hypothèses sans fondement et les fonctions hors périmètre.
- Si le plan comporte encore des lacunes, l’agent joue le rôle d’architecte et m’interroge. Il pose au plus cinq questions, uniquement sur ce qui bloque la mise en œuvre, et justifie chacune d’elles.
J’ai rédigé l’exemple de prompt ci-dessous pour cet article, selon cette méthode. Le prompt d’origine, celui qui a servi à créer la fonction de QRIS dynamique, n’a pas été conservé.
Vous êtes un senior pragmatic software engineer. Donnez la priorité à l’exactitude, à la sécurité, à la facilité de maintenance et à une architecture simple. N’inventez pas de faits. Si un détail vous bloque, posez une seule question courte.
Objectif : créez la fonction generateDynamicQris dans js/engine/qris.js, qui transforme le QRIS statique de la boutique en QRIS dynamique avec montant, entièrement sur l’appareil, sans serveur.
Contexte : lisez js/engine/qris.js et js/pages/pos.js avant de modifier quoi que ce soit. Le contenu du QRIS suit le format ID, longueur et valeur de la spécification EMVCo Merchant-Presented Mode.
Contraintes : ne modifiez pas les données du compte marchand (ID 26 à 51). Le montant doit être un entier positif. N’ajoutez aucune dépendance et ne touchez à aucun autre fichier.
Étapes : changez la valeur de l’ID 01 de 11 à 12 ; supprimez l’ancien ID 54 s’il existe ; insérez le nouvel ID 54 juste avant l’ID 58 en lisant la structure des données, et non en cherchant du texte ; recalculez le CRC-16/CCITT avec le polynôme 0x1021 et la valeur initiale 0xFFFF. Faites une étape, vérifiez-la, puis passez à la suivante.
Vérification : la fonction CRC doit renvoyer 29B1 pour le texte 123456789. Testez aussi avec des données fictives, et non avec le QRIS d’une vraie boutique.
Rapport : fichiers modifiés, raisons, commandes de test et leurs résultats, risques et hypothèses.
Le cœur technique : un QRIS dynamique créé sur le téléphone
Le code QR d’un QRIS contient un texte composé de data objects (objets de données). Chaque data object se compose d’un ID à deux chiffres, d’une longueur à deux chiffres, puis de sa valeur. Ce format vient de la spécification EMV QR Code Merchant-Presented Mode d’EMVCo (site externe). Selon la documentation pour développeurs de Mastercard (site externe), qui renvoie à cette spécification, l’ID 01 vaut 11 pour un QR statique et 12 pour un QR dynamique. L’ID 54 contient le montant de la transaction, l’ID 58 le code pays, et l’ID 63 le CRC, qui doit être le dernier data object.
Toutes les valeurs du tableau suivant sont des données fictives.
| ID | Signification | QRIS statique | QRIS dynamique |
|---|---|---|---|
| 00 | Version du format | 01 | 01 |
| 01 | Type de QR | 11 | 12 |
| 26 à 51 | Données du compte marchand fournies par le prestataire QRIS | Telles quelles | Non modifiées |
| 54 | Montant | Absent | 25000 |
| 58 | Code pays | ID | ID |
| 59 | Nom du marchand | TOKO CONTOH | TOKO CONTOH |
| 63 | CRC | Calculé sur le contenu du QR statique | Recalculé |
La fonction de QRIS-EDC modifie l’ID 01, insère l’ID 54, puis recalcule le CRC. J’ai simplifié l’esquisse suivante :
function buatDinamis(qris, rp) {
// ôte "6304" et l'ancien CRC
let s = qris.slice(0, -8);
// ID 01: 11 (statique) -> 12
s = s.replace('010211',
'010212');
const n = String(rp);
const tag54 = '54' +
String(n.length)
.padStart(2, '0') + n;
// insère ID 54 avant ID 58
const i = posisiTag(s, '58');
s = s.slice(0, i) + tag54
+ s.slice(i) + '6304';
return s + crc16(s);
}
La fonction posisiTag lit le texte depuis le début, data object par data object, jusqu’à trouver l’ID 58. Le nombre 58 peut apparaître dans une autre valeur, par exemple le numéro du marchand : une simple recherche de texte risquerait donc d’insérer le montant au mauvais endroit. Le code d’origine supprime aussi l’ancien ID 54 s’il existe et refuse tout montant qui n’est pas un nombre entier de roupies.
Le CRC se calcule sur tout le texte jusqu’à 6304 inclus, c’est-à-dire l’ID 63 suivi de la longueur 04. QRIS-EDC utilise le CRC-16/CCITT avec le polynôme 0x1021 et la valeur initiale 0xFFFF, puis écrit le résultat en quatre chiffres hexadécimaux majuscules. Pour tester votre fonction CRC, calculez-la sur le texte 123456789 : le résultat doit être 29B1.
Limites de cette méthode
Le QR produit par cette fonction reprend les données du compte marchand de l’autocollant QRIS de la boutique. Le prestataire QRIS de la boutique traite le paiement, et l’argent arrive sur le compte relié à cet autocollant. QRIS-EDC ne détient pas d’argent et n’est pas relié au système du prestataire : l’application ne sait donc pas si le paiement a abouti. Le caissier vérifie toujours l’argent reçu dans l’application de la banque ou du portefeuille électronique de la boutique avant d’enregistrer la transaction. Le système du prestataire QRIS n’émet pas non plus ce QR, et je ne prétends pas que Bank Indonesia ou l’ASPI (l’association indonésienne des systèmes de paiement) aient approuvé cette méthode.
Les décisions que j’ai prises moi-même
Turso refuse le PRAGMA de SQLAlchemy
Le backend utilise SQLAlchemy avec Turso (libSQL) via le paquet sqlalchemy-libsql. À la première connexion, le dialecte SQLite de SQLAlchemy exécute PRAGMA read_uncommitted pour lire le niveau d’isolation des transactions. Quand on y accède par le protocole Hrana, Turso refuse cette commande, et la connexion échoue avec une ValueError 405.
La correction figure dans l’historique git du fichier app/db.py le 14 mai 2026, en sept commits entre 3 h 54 et 4 h 19 WIB (UTC+7). J’ai désactivé pool_pre_ping, puis j’ai enveloppé la méthode get_isolation_level pour qu’elle renvoie SERIALIZABLE, le niveau d’isolation par défaut de SQLite, quand cette erreur apparaît. Après avoir essayé plusieurs formats d’URL, je suis revenu au format de la documentation SQLAlchemy de Turso (site externe). Le schéma ressemble à ceci, avec des valeurs de substitution pour l’hôte et le jeton :
from sqlalchemy import \
create_engine
from sqlalchemy.dialects \
import sqlite
D = sqlite.base.SQLiteDialect
asli = D.get_isolation_level
def aman(self, conn):
try:
return asli(self, conn)
except Exception:
return "SERIALIZABLE"
D.get_isolation_level = aman
engine = create_engine(
"sqlite+libsql://HOST_DB"
"?secure=true",
# TOKEN lu dans l'env
connect_args={
"auth_token": TOKEN},
pool_pre_ping=False,
)
Un plan de télémétrie pour un million d’appareils
QRIS-EDC propose une fonction facultative qui envoie les prix des produits de première nécessité, ou sembako (pour le suivi de l’inflation), et un résumé mensuel des transactions (pour une évaluation alternative du crédit). Cette fonction ne s’active que si l’utilisateur donne son accord. Le 6 mars 2026, j’ai rédigé deux plans de backend, TELEMETRY_BE_PLAN.md et TELEMETRY_PYTHON_ENTERPRISE_PLAN.md, avec pour cible « 1 000 000+ nœuds ». Ces plans prévoyaient Apache Kafka comme file d’attente, Redis, InfluxDB pour les séries temporelles de prix, PostgreSQL, et Kubernetes pour ajouter de la capacité automatiquement.
Pour la démo du hackathon, j’ai choisi une conception bien plus petite : un seul monolithe FastAPI, selon IMPLEMENTATION_PLAN_SERVER.md. Ce document se présente comme un MVP pour le hackathon et la démo, et donne trois raisons : rapide à construire, léger et facile à faire passer plus tard sur PostgreSQL. Ce plan repoussait Redis, Celery, PostgreSQL et les modèles complexes d’évaluation du crédit.
J’ai fait tourner ce backend sur Vercel avec une base de données Turso. Le document de déploiement du projet note les limites du serverless de Vercel, qui ont en partie déterminé la forme du backend :
- Un fichier SQLite ne persiste pas dans les fonctions serverless de Vercel : la base de données de production utilise donc Turso.
- Aucune tâche de fond ne tourne en continu. Le nettoyage des anciennes données s’exécute au hasard sur environ 2 % des requêtes.
- Il vaut mieux lancer les migrations Alembic à la main avant le déploiement, pour éviter que plusieurs démarrages à froid (cold starts) exécutent des migrations en même temps. La migration automatique au démarrage de l’application sert seulement de solution de secours.
Cet article est un retour d’expérience sur la création d’une application et ne remplace pas les règles officielles du QRIS. Les extraits de code ci-dessus sont des esquisses simplifiées. Pour accepter les paiements QRIS, suivez les conditions de la banque ou du prestataire QRIS de votre boutique.
Sources
- Page du produit QRIS-EDC, HanzCodes (site externe)
- Codex, OpenAI (site externe)
- SQLAlchemy + Turso, documentation de Turso (site externe)
- EMV QR Code Specification for Payment Systems (EMV QRCPS) Merchant-Presented Mode, EMVCo (site externe)
- Generate QR Code for Merchants, Mastercard Developers (site externe)
Besoin d’aide ?
Décrivez-nous les besoins de votre administration ou de votre entreprise. Le premier échange est gratuit.