Membuat aplikasi kasir QRIS dengan Codex: catatan pembuatan QRIS-EDC dari Maret sampai Juli 2026
Saya Mohammad Hilmi Bisri, pendiri PT Hanz Kode Terpadu (HanzCodes) di Jombang. Menurut tanggal file proyek, saya membuat QRIS-EDC dari Maret sampai Juli 2026. QRIS-EDC adalah aplikasi kasir gratis yang berjalan di browser HP dan menampilkan QRIS dengan nominal yang pas untuk setiap transaksi. Sebagian besar kodenya saya tulis bersama OpenAI Codex (situs luar), agen pemrograman yang menurut OpenAI bisa dipakai di ChatGPT, editor kode, dan terminal.
Folder proyek tidak menyimpan log percakapan dengan Codex, jadi peran Codex di sini berdasarkan keterangan saya sendiri. Urutan kerja dan detail teknis saya ambil dari tanggal file di empat folder kerja dan dari riwayat git backend. Kalau Anda pemilik toko yang sedang memilih cara menerima QRIS, perbandingannya ada di artikel aplikasi kasir QRIS gratis.
Awalnya QRIS-EDC adalah karya saya untuk PIDI Digdaya X Hackathon 2026 dari Bank Indonesia dan OJK, dengan nama kerja Kasir Digdaya. Karya ini tidak masuk 80 besar. Setelah hackathon, saya terus mengembangkannya dan merilisnya sebagai aplikasi gratis.
Urutan pengerjaan
| Waktu | Yang dikerjakan |
|---|---|
| 1 Maret 2026 | Versi pertama: satu halaman HTML dengan satu file JavaScript dan satu file CSS. |
| 2 Maret 2026 | Aplikasi dipecah menjadi beberapa halaman: kasir, barang, laporan, pemindai QRIS, dan pengaturan. |
| 4 sampai 6 Maret 2026 | Dibangun ulang sebagai SPA (aplikasi satu halaman) yang modular. Folder engine berisi logika QRIS, stok, printer, dan sinkronisasi, folder pages berisi tiap layar, dan folder lib berisi penyimpanan dan utilitas. Beberapa fitur besar saya rencanakan dulu dalam file Markdown, misalnya laporan dan persetujuan pengiriman data. |
| Mei 2026 | Backend FastAPI dengan database Turso, dijalankan di Vercel. Repositorinya berisi 33 commit, semuanya pada 14 dan 19 Mei. Di sisi aplikasi, saya menambahkan WebLLM, pustaka untuk menjalankan model bahasa kecil langsung di browser. |
| Juli 2026 | Catatan hutang pelanggan (kasbon) dan tur panduan di dalam aplikasi. |
QRIS-EDC memakai model bahasa itu untuk mengubah pesanan lisan, misalnya "dua kopi, gula kurang", menjadi isi keranjang. Pengguna memilih modelnya sendiri, misalnya Qwen atau Gemma versi kecil. Browser mengunduh bobot model ke cache saat model itu pertama dipakai.
Cara saya menulis prompt
Pada Mei 2026 saya menuliskan pola prompt saya dalam dokumen panduan internal:
- Prompt induk memberi agen peran senior pragmatic software engineer yang mengutamakan kebenaran (correctness), keamanan, kemudahan perawatan, dan arsitektur sederhana. Agen dilarang mengarang fakta. Kalau ada detail yang menghalangi pekerjaan, agen mengajukan satu pertanyaan singkat.
- Agen mengerjakan satu tugas, memverifikasinya, baru pindah ke tugas berikutnya. Saya tidak meminta pekerjaan paralel dan tidak menggabungkan perubahan yang tidak berhubungan dalam satu permintaan.
- Sebelum menulis kode, agen menulis ulang ide kasar saya menjadi spesifikasi teknis: peran, tujuan, konteks, batasan, file yang perlu dibaca, urutan kerja, cara verifikasi, dan definisi selesai. Pada tahap ini agen juga membuang kata-kata promosi, asumsi tanpa dasar, dan fitur di luar cakupan.
- Kalau rencananya masih berlubang, agen berperan sebagai arsitek dan mewawancarai saya. Pertanyaannya paling banyak lima, hanya yang menghalangi implementasi, dan setiap pertanyaan disertai alasan.
Contoh prompt di bawah saya susun untuk artikel ini dengan pola tersebut. Prompt asli saat saya membuat fitur QRIS dinamis tidak tersimpan.
Anda adalah senior pragmatic software engineer. Utamakan kebenaran, keamanan, kemudahan perawatan, dan arsitektur sederhana. Jangan mengarang fakta. Kalau ada detail yang menghalangi, tanyakan satu pertanyaan singkat.
Tujuan: buat fungsi generateDynamicQris di js/engine/qris.js yang mengubah QRIS statis toko menjadi QRIS dinamis bernominal, sepenuhnya di perangkat, tanpa server.
Konteks: baca js/engine/qris.js dan js/pages/pos.js sebelum mengubah apa pun. Isi QRIS memakai format ID, panjang, dan nilai dari spesifikasi EMVCo Merchant-Presented Mode.
Batasan: jangan mengubah data akun merchant (ID 26 sampai 51). Nominal wajib bilangan bulat positif. Jangan menambah dependensi dan jangan menyentuh file lain.
Langkah: ubah nilai ID 01 dari 11 menjadi 12; hapus ID 54 lama kalau ada; sisipkan ID 54 baru tepat sebelum ID 58 dengan membaca struktur data, bukan mencari teks; hitung ulang CRC-16/CCITT dengan polinomial 0x1021 dan nilai awal 0xFFFF. Kerjakan satu langkah, verifikasi, baru lanjut.
Verifikasi: fungsi CRC harus menghasilkan 29B1 untuk teks 123456789. Uji juga dengan data buatan, bukan QRIS milik toko sungguhan.
Laporan: file yang diubah, alasannya, perintah uji beserta hasilnya, risiko, dan asumsi.
Inti teknis: QRIS dinamis dibuat di HP
Isi kode QR pada QRIS adalah teks yang tersusun dari data object. Setiap data object terdiri atas ID dua digit, panjang dua digit, lalu nilainya. Format ini berasal dari spesifikasi EMV QR Code Merchant-Presented Mode dari EMVCo (situs luar). Menurut dokumentasi pengembang Mastercard (situs luar) yang merujuk spesifikasi itu, ID 01 bernilai 11 untuk QR statis dan 12 untuk QR dinamis. ID 54 berisi nominal transaksi, ID 58 berisi kode negara, dan ID 63 berisi CRC yang harus menjadi data object terakhir.
Semua nilai di tabel berikut adalah data buatan.
| ID | Arti | QRIS statis | QRIS dinamis |
|---|---|---|---|
| 00 | Versi format | 01 | 01 |
| 01 | Jenis QR | 11 | 12 |
| 26 sampai 51 | Data akun merchant dari penyedia QRIS | Apa adanya | Tidak diubah |
| 54 | Nominal | Tidak ada | 25000 |
| 58 | Kode negara | ID | ID |
| 59 | Nama merchant | TOKO CONTOH | TOKO CONTOH |
| 63 | CRC | Dihitung dari isi QR statis | Dihitung ulang |
Fungsi di QRIS-EDC mengubah ID 01, menyisipkan ID 54, lalu menghitung ulang CRC. Sketsa berikut sudah saya sederhanakan:
function buatDinamis(qris, rp) {
// buang "6304" dan CRC lama
let s = qris.slice(0, -8);
// ID 01: 11 (statis) jadi 12
s = s.replace('010211',
'010212');
const n = String(rp);
const tag54 = '54' +
String(n.length)
.padStart(2, '0') + n;
// sisipkan ID 54 sebelum 58
const i = posisiTag(s, '58');
s = s.slice(0, i) + tag54
+ s.slice(i) + '6304';
return s + crc16(s);
}
Fungsi posisiTag membaca teks dari awal per data object sampai menemukan ID 58. Angka 58 bisa muncul di dalam nilai lain, misalnya nomor merchant, jadi pencarian teks biasa bisa menyisipkan nominal di tempat yang salah. Kode aslinya juga menghapus ID 54 lama kalau ada dan menolak nominal yang bukan bilangan bulat rupiah.
CRC dihitung atas seluruh teks sampai dan termasuk 6304, yaitu ID 63 dengan panjang 04. QRIS-EDC memakai CRC-16/CCITT dengan polinomial 0x1021 dan nilai awal 0xFFFF, lalu menulis hasilnya sebagai empat digit heksadesimal huruf besar. Untuk menguji fungsi CRC Anda, hitung teks 123456789: hasilnya harus 29B1.
Batas cara ini
QR hasil fungsi ini memakai data akun merchant dari stiker QRIS toko. Penyedia QRIS toko memproses pembayarannya, dan uang masuk ke rekening yang terhubung dengan stiker itu. QRIS-EDC tidak memegang uang dan tidak terhubung ke sistem penyedia, sehingga aplikasi tidak tahu apakah pembayaran berhasil. Kasir tetap memeriksa uang masuk di aplikasi bank atau dompet digital toko sebelum menyimpan transaksi. Sistem penyedia QRIS juga tidak menerbitkan QR ini, dan saya tidak mengklaim cara ini disetujui Bank Indonesia atau ASPI.
Masalah yang saya putuskan sendiri
Turso menolak PRAGMA dari SQLAlchemy
Backend memakai SQLAlchemy dengan Turso (libSQL) lewat paket sqlalchemy-libsql. Saat koneksi pertama, dialek SQLite di SQLAlchemy menjalankan PRAGMA read_uncommitted untuk membaca tingkat isolasi transaksi. Turso yang diakses lewat protokol Hrana menolak perintah itu, dan koneksi gagal dengan ValueError 405.
Perbaikannya tercatat di riwayat git file app/db.py pada 14 Mei 2026, dalam tujuh commit antara pukul 03.54 dan 04.19 WIB. Saya mematikan pool_pre_ping, lalu membungkus metode get_isolation_level supaya metode itu mengembalikan SERIALIZABLE, tingkat isolasi bawaan SQLite, saat galat tersebut muncul. Setelah mencoba beberapa format URL, saya kembali ke format di dokumentasi SQLAlchemy untuk Turso (situs luar). Polanya seperti ini, dengan host dan token sebagai placeholder:
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 dibaca dari env
connect_args={
"auth_token": TOKEN},
pool_pre_ping=False,
)
Rencana telemetri untuk sejuta perangkat
QRIS-EDC punya fitur opsional untuk mengirim data harga sembako (untuk pemantauan inflasi) dan ringkasan transaksi bulanan (untuk penilaian kredit alternatif). Fitur ini hanya aktif kalau pengguna menyetujuinya. Pada 6 Maret 2026 saya menyusun dua rencana backend, TELEMETRY_BE_PLAN.md dan TELEMETRY_PYTHON_ENTERPRISE_PLAN.md, dengan target "1.000.000+ node". Rancangannya memakai Apache Kafka sebagai antrean, Redis, InfluxDB untuk data harga berbasis waktu, PostgreSQL, dan Kubernetes untuk menambah kapasitas secara otomatis.
Untuk demo hackathon, saya memilih rancangan yang jauh lebih kecil: satu monolit FastAPI sesuai IMPLEMENTATION_PLAN_SERVER.md. Dokumen itu menyebut dirinya MVP untuk hackathon dan demo, dengan alasan cepat dibuat, ringan, dan mudah dipindah ke PostgreSQL nanti. Rencana itu menunda Redis, Celery, PostgreSQL, dan model penilaian kredit yang rumit.
Saya menjalankan backend itu di Vercel dengan database Turso. Dokumen deploy proyek mencatat batasan serverless Vercel yang ikut menentukan bentuknya:
- File SQLite tidak bertahan di fungsi serverless Vercel, jadi database production memakai Turso.
- Tidak ada background task yang terus hidup. Pembersihan data lama dijalankan secara acak pada sekitar 2% request.
- Migrasi Alembic sebaiknya dijalankan manual sebelum deploy, supaya beberapa cold start tidak menjalankan migrasi bersamaan. Migrasi otomatis saat aplikasi mulai hanya menjadi cadangan.
Artikel ini catatan pengalaman membuat satu aplikasi dan tidak menggantikan ketentuan resmi QRIS. Potongan kode di atas adalah sketsa yang sudah disederhanakan. Untuk menerima pembayaran QRIS, ikuti ketentuan bank atau penyedia QRIS toko Anda.
Sumber
Butuh bantuan?
Ceritakan kebutuhan instansi atau usaha Anda. Diskusi awal tidak dipungut biaya.