Creare un'app di cassa QRIS con Codex: appunti sullo sviluppo di QRIS-EDC da marzo a luglio 2026
Sono Mohammad Hilmi Bisri, fondatore di PT Hanz Kode Terpadu (HanzCodes) a Jombang. Secondo le date dei file del progetto, ho sviluppato QRIS-EDC da marzo a luglio 2026. QRIS-EDC è un'app di cassa gratuita che funziona nel browser dello smartphone e mostra per ogni transazione un QRIS (lo standard indonesiano per i pagamenti con codice QR) con l'importo esatto. Gran parte del codice l'ho scritta insieme a OpenAI Codex (sito esterno), un agente di programmazione che secondo OpenAI si può usare in ChatGPT, negli editor di codice e nel terminale.
La cartella del progetto non conserva i registri delle conversazioni con Codex, quindi il ruolo di Codex descritto qui si basa sul mio racconto. Le fasi di lavoro e i dettagli tecnici li ho ricavati dalle date dei file in quattro cartelle di lavoro e dalla cronologia git del backend. Se Lei è titolare di un negozio e sta scegliendo come accettare il QRIS, trova il confronto tra le possibilità nell'articolo app di cassa QRIS gratuita.
In origine QRIS-EDC era il mio progetto per il PIDI Digdaya X Hackathon 2026 di Bank Indonesia e OJK (la banca centrale indonesiana e l'autorità di vigilanza sui servizi finanziari), con il nome provvisorio Kasir Digdaya. Il progetto non è entrato tra i primi 80. Dopo l'hackathon ho continuato a svilupparlo e l'ho pubblicato come app gratuita.
Le fasi di lavoro
| Periodo | Lavoro svolto |
|---|---|
| 1° marzo 2026 | Prima versione: una pagina HTML con un file JavaScript e un file CSS. |
| 2 marzo 2026 | Divisione dell'app in più pagine: cassa, prodotti, rapporti, lettore QRIS e impostazioni. |
| Dal 4 al 6 marzo 2026 | Ricostruzione dell'app come SPA (applicazione a pagina singola) modulare. La cartella engine contiene la logica per QRIS, scorte, stampante e sincronizzazione, la cartella pages contiene le singole schermate e la cartella lib contiene archiviazione e funzioni di utilità. Alcune funzioni importanti le ho pianificate prima in file Markdown, per esempio i rapporti e il consenso all'invio dei dati. |
| Maggio 2026 | Backend FastAPI con database Turso, eseguito su Vercel. Il repository contiene 33 commit, tutti del 14 e del 19 maggio. Nell'app ho aggiunto WebLLM, una libreria che esegue piccoli modelli linguistici direttamente nel browser. |
| Luglio 2026 | Registro dei debiti dei clienti (kasbon) e tour guidato all'interno dell'app. |
QRIS-EDC usa questi modelli linguistici per trasformare un ordine a voce, per esempio «due caffè, poco zucchero», nel contenuto del carrello. L'utente sceglie da sé il modello, per esempio una versione piccola di Qwen o di Gemma. Il browser scarica i pesi del modello nella cache la prima volta che quel modello viene usato.
Come scrivo i prompt
A maggio 2026 ho messo per iscritto il mio schema dei prompt in un documento guida interno:
- Il prompt principale assegna all'agente il ruolo di senior pragmatic software engineer, che mette al primo posto correttezza (correctness), sicurezza, facilità di manutenzione e architettura semplice. All'agente è vietato inventare fatti. Se un dettaglio blocca il lavoro, l'agente fa una sola domanda breve.
- L'agente svolge un compito, lo verifica e solo dopo passa al successivo. Non chiedo lavori in parallelo e non metto nella stessa richiesta modifiche che non hanno legami tra loro.
- Prima di scrivere il codice, l'agente riscrive la mia idea grezza come specifica tecnica: ruolo, obiettivo, contesto, vincoli, file da leggere, ordine di lavoro, modo di verifica e definizione di completamento. In questa fase l'agente elimina anche il linguaggio promozionale, le ipotesi senza fondamento e le funzioni fuori ambito.
- Se il piano ha ancora lacune, l'agente assume il ruolo di architetto e mi intervista. Fa al massimo cinque domande, solo su ciò che blocca l'implementazione, e motiva ciascuna domanda.
Ho composto il prompt di esempio qui sotto per questo articolo, seguendo quello schema. Il prompt originale che ho usato per creare la funzione del QRIS dinamico non si è conservato.
Lei è un senior pragmatic software engineer. Dia priorità a correttezza, sicurezza, facilità di manutenzione e architettura semplice. Non inventi fatti. Se un dettaglio La blocca, faccia una sola domanda breve.
Obiettivo: crei la funzione generateDynamicQris in js/engine/qris.js, che trasformi il QRIS statico del negozio in un QRIS dinamico con importo, interamente sul dispositivo e senza server.
Contesto: legga js/engine/qris.js e js/pages/pos.js prima di modificare qualsiasi cosa. Il contenuto del QRIS usa il formato ID, lunghezza e valore della specifica EMVCo Merchant-Presented Mode.
Vincoli: non modifichi i dati dell'account dell'esercente (ID da 26 a 51). L'importo deve essere un numero intero positivo. Non aggiunga dipendenze e non tocchi altri file.
Passi: cambi il valore dell'ID 01 da 11 a 12; elimini il vecchio ID 54, se presente; inserisca il nuovo ID 54 subito prima dell'ID 58 leggendo la struttura dei dati, non cercando nel testo; ricalcoli il CRC-16/CCITT con polinomio 0x1021 e valore iniziale 0xFFFF. Esegua un passo, lo verifichi e solo dopo prosegua.
Verifica: la funzione CRC deve restituire 29B1 per il testo 123456789. Esegua le prove anche con dati fittizi, non con il QRIS di un negozio reale.
Rapporto: i file modificati, il motivo, i comandi di prova con i risultati, i rischi e le ipotesi.
Il nucleo tecnico: il QRIS dinamico creato sullo smartphone
Il contenuto del codice QR di un QRIS è un testo composto da data object. Ogni data object è formato da un ID di due cifre, da una lunghezza di due cifre e poi dal valore. Questo formato deriva dalla specifica EMV QR Code Merchant-Presented Mode di EMVCo (sito esterno). Secondo la documentazione per sviluppatori di Mastercard (sito esterno), che fa riferimento a quella specifica, l'ID 01 vale 11 per i QR statici e 12 per i QR dinamici. L'ID 54 contiene l'importo della transazione, l'ID 58 il codice del paese e l'ID 63 il CRC, che deve essere l'ultimo data object.
Tutti i valori della tabella seguente sono dati fittizi.
| ID | Significato | QRIS statico | QRIS dinamico |
|---|---|---|---|
| 00 | Versione del formato | 01 | 01 |
| 01 | Tipo di QR | 11 | 12 |
| Da 26 a 51 | Dati dell'account dell'esercente assegnati dal fornitore del QRIS | Così come sono | Invariati |
| 54 | Importo | Assente | 25000 |
| 58 | Codice del paese | ID | ID |
| 59 | Nome dell'esercente | TOKO CONTOH | TOKO CONTOH |
| 63 | CRC | Calcolato sul contenuto del QR statico | Ricalcolato |
La funzione di QRIS-EDC modifica l'ID 01, inserisce l'ID 54 e poi ricalcola il CRC. Ho semplificato la bozza seguente:
function buatDinamis(qris, rp) {
// togli "6304" e CRC vecchio
let s = qris.slice(0, -8);
// ID 01: da 11 (statico) a 12
s = s.replace('010211',
'010212');
const n = String(rp);
const tag54 = '54' +
String(n.length)
.padStart(2, '0') + n;
// inserisci ID 54 prima di 58
const i = posisiTag(s, '58');
s = s.slice(0, i) + tag54
+ s.slice(i) + '6304';
return s + crc16(s);
}
La funzione posisiTag legge il testo dall'inizio, un data object alla volta, finché non trova l'ID 58. Il numero 58 può comparire dentro altri valori, per esempio nel numero dell'esercente, quindi una semplice ricerca nel testo potrebbe inserire l'importo nel punto sbagliato. Il codice originale elimina anche il vecchio ID 54, se presente, e rifiuta gli importi che non sono un numero intero di rupie.
Il CRC si calcola su tutto il testo fino a 6304 compreso, cioè l'ID 63 con lunghezza 04. QRIS-EDC usa il CRC-16/CCITT con polinomio 0x1021 e valore iniziale 0xFFFF, poi scrive il risultato come quattro cifre esadecimali maiuscole. Per provare la Sua funzione CRC, la applichi al testo 123456789: il risultato deve essere 29B1.
I limiti di questo metodo
Il QR creato da questa funzione usa i dati dell'account dell'esercente presi dall'adesivo QRIS del negozio. Il pagamento lo elabora il fornitore del QRIS del negozio, e il denaro arriva sul conto collegato a quell'adesivo. QRIS-EDC non detiene denaro e non è collegato al sistema del fornitore, quindi l'app non sa se il pagamento è andato a buon fine. Prima di salvare la transazione, il cassiere controlla comunque gli incassi nell'app della banca o del portafoglio digitale del negozio. Inoltre il sistema del fornitore del QRIS non emette questo QR, e non affermo che questo metodo sia approvato da Bank Indonesia o dall'ASPI (l'associazione indonesiana dei sistemi di pagamento).
Le decisioni che ho preso da solo
Turso rifiuta il PRAGMA di SQLAlchemy
Il backend usa SQLAlchemy con Turso (libSQL) tramite il pacchetto sqlalchemy-libsql. Alla prima connessione, il dialetto SQLite di SQLAlchemy esegue PRAGMA read_uncommitted per leggere il livello di isolamento delle transazioni. Turso, quando vi si accede tramite il protocollo Hrana, rifiuta quel comando, e la connessione fallisce con un ValueError 405.
La cronologia git del file app/db.py registra la correzione il 14 maggio 2026, in sette commit tra le 3:54 e le 4:19 WIB (UTC+7). Ho disattivato pool_pre_ping, poi ho avvolto il metodo get_isolation_level in modo che restituisca SERIALIZABLE, il livello di isolamento predefinito di SQLite, quando compare quell'errore. Dopo aver provato alcuni formati di URL, sono tornato al formato indicato nella documentazione di SQLAlchemy per Turso (sito esterno). Lo schema è questo, con host e token come segnaposto:
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 letto da env
connect_args={
"auth_token": TOKEN},
pool_pre_ping=False,
)
Il piano di telemetria per un milione di dispositivi
QRIS-EDC ha una funzione facoltativa per inviare i prezzi dei generi di prima necessità (per il monitoraggio dell'inflazione) e riepiloghi mensili delle transazioni (per una valutazione alternativa del merito creditizio). La funzione si attiva solo se l'utente dà il consenso. Il 6 marzo 2026 ho preparato due piani per il backend, TELEMETRY_BE_PLAN.md e TELEMETRY_PYTHON_ENTERPRISE_PLAN.md, con l'obiettivo di «1.000.000+ node». Il progetto prevedeva Apache Kafka come coda, Redis, InfluxDB per i prezzi come serie temporali, PostgreSQL e Kubernetes per aumentare la capacità in modo automatico.
Per la demo dell'hackathon ho scelto un progetto molto più piccolo: un unico monolite FastAPI, come descritto in IMPLEMENTATION_PLAN_SERVER.md. Quel documento si definisce un MVP per l'hackathon e la demo, e motiva la scelta con la rapidità di realizzazione, la leggerezza e la facilità di passare in seguito a PostgreSQL. Il piano rimandava Redis, Celery, PostgreSQL e un modello complesso di valutazione del credito.
Ho eseguito quel backend su Vercel con un database Turso. La documentazione di deploy del progetto riporta i limiti serverless di Vercel che ne hanno influenzato la forma:
- I file SQLite non persistono nelle funzioni serverless di Vercel, quindi il database di produzione usa Turso.
- Non ci sono attività in background sempre attive. Il backend pulisce i dati vecchi in modo casuale, su circa il 2% delle richieste.
- È meglio eseguire a mano le migrazioni Alembic prima del deploy, così più cold start non le eseguono nello stesso momento. La migrazione automatica all'avvio dell'app serve solo come riserva.
Questo articolo racconta l'esperienza di sviluppo di una singola app e non sostituisce le disposizioni ufficiali sul QRIS. I frammenti di codice qui sopra sono bozze semplificate. Per accettare pagamenti QRIS segua le disposizioni della banca o del fornitore del QRIS del Suo negozio.
Fonti
- Pagina del prodotto QRIS-EDC, HanzCodes (sito esterno)
- Codex, OpenAI (sito esterno)
- SQLAlchemy + Turso, documentazione di Turso (sito esterno)
- EMV QR Code Specification for Payment Systems (EMV QRCPS) Merchant-Presented Mode, EMVCo (sito esterno)
- Generate QR Code for Merchants, Mastercard Developers (sito esterno)
Le serve aiuto?
Ci descriva le esigenze del Suo ente o della Sua attività. Il primo colloquio è gratuito.