Langsung ke isi
PT Hanz Kode Terpadu, ke beranda

Membuat aplikasi kasir QRIS dengan Codex: catatan pembuatan QRIS-EDC dari Maret sampai Juli 2026

Oleh Mohammad Hilmi Bisri, S.Kom.. Terbit 24 Oktober 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

Urutan pengerjaan QRIS-EDC menurut tanggal file, Maret sampai Juli 2026
WaktuYang dikerjakan
1 Maret 2026Versi pertama: satu halaman HTML dengan satu file JavaScript dan satu file CSS.
2 Maret 2026Aplikasi dipecah menjadi beberapa halaman: kasir, barang, laporan, pemindai QRIS, dan pengaturan.
4 sampai 6 Maret 2026Dibangun 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 2026Backend 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 2026Catatan 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Contoh data object QRIS statis dan QRIS dinamis dengan data buatan
IDArtiQRIS statisQRIS dinamis
00Versi format0101
01Jenis QR1112
26 sampai 51Data akun merchant dari penyedia QRISApa adanyaTidak diubah
54NominalTidak ada25000
58Kode negaraIDID
59Nama merchantTOKO CONTOHTOKO CONTOH
63CRCDihitung dari isi QR statisDihitung 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:

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.

WhatsApp (situs luar)