Membuat Billing Rental PS dengan Java Swing: Timer Anti Reset, Aturan Tarif, dan Satu Unit Satu Sesi
Membuat Billing Rental PS dengan Java Swing: Timer Anti Reset, Aturan Tarif, dan Satu Unit Satu Sesi
Aplikasi billing rental PS kelihatannya cuma deretan stopwatch. Masalahnya muncul di lapangan: listrik padam dan timer kembali ke nol, penjaga lupa bahwa unit 7 sudah habis waktunya sepuluh menit lalu, dua penjaga memulai sesi di unit yang sama, atau pemilik ragu apakah penjaga menagih sesuai tarif. Artikel ini membahas lima teknik inti dari aplikasi Billing PS berbasis Java Swing dan SQLite.
1. Simpan waktu mulai, bukan sisa waktu
Kesalahan paling umum adalah menyimpan "sisa waktu" lalu menguranginya setiap detik dengan Timer. Begitu aplikasi ditutup, hitungannya hilang. Yang benar: simpan kapan sesi dimulai dan berapa lama dijeda, lalu hitung lama main kapan pun dibutuhkan.
public long durasiMs(long sekarang) {
long akhir = selesai != null ? selesai : sekarang;
long jeda = jedaMs + (jedaSejak > 0 && selesai == null ? sekarang - jedaSejak : 0);
return Math.max(0, akhir - mulai - jeda);
}
public long sisaMs(long sekarang) {
return PAKET.equals(mode) ? menitPaket * 60_000L - durasiMs(sekarang) : 0;
}
Kolom mulai, jeda_ms, dan jeda_sejak disimpan sebagai epoch milidetik. Saat dijeda, jeda_sejak diisi waktu sekarang; saat dilanjutkan, selisihnya ditambahkan ke jeda_ms. javax.swing.Timer di layar hanya bertugas menggambar ulang setiap detik dari rumus di atas. Akibatnya aplikasi boleh ditutup, laptop boleh mati, operator boleh ganti shift; saat dibuka lagi, angka di layar tetap benar.
Semua service menerima java.time.Clock dari luar. Di aplikasi dipakai Clock.systemDefaultZone(), sedangkan di tes dipakai jam buatan yang bisa dimajukan, misalnya jam.maju(65), sehingga skenario dua jam bisa diuji dalam sepersekian detik.
2. Aturan tarif dalam satu kelas murni
Setiap rental punya kebijakan sendiri: dibulatkan per 15 menit atau per 30 menit, minimal main setengah jam, toleransi beberapa menit untuk paket. Aturan ini dipisahkan ke kelas tanpa database:
public static Hasil open(long durasiMs, long tarifJam, Aturan a) {
int menit = menitMain(durasiMs);
int tagih = Math.max(a.minimalMenit(), bulatBlok(menit, a.blokMenit()));
long biaya = bulat500(tagih * tarifJam / 60.0);
return new Hasil(biaya, tagih, 0, "Open " + tagih + " menit x Rp" + tarifJam + "/jam");
}
public static Hasil paket(long durasiMs, int menitPaket, long hargaPaket, long tarifJam, Aturan a) {
int menit = menitMain(durasiMs);
int lebih = menit - menitPaket;
if (lebih <= a.toleransiMenit()) return new Hasil(hargaPaket, menitPaket, 0, "Paket " + menitPaket + " menit");
int tagihLebih = bulatBlok(lebih, a.blokMenit());
long biayaLebih = bulat500(tagihLebih * tarifJam / 60.0);
(hargaPaket + biayaLebih, menitPaket + tagihLebih, tagihLebih, );
}
Contoh: PS4 Rp7.000 per jam, main 61 menit dengan open billing. Menit dibulatkan ke blok 15 menjadi 75 menit, 75 x 7.000 / 60 = Rp8.750, lalu dibulatkan ke atas per Rp500 menjadi Rp9.000. Karena kelasnya murni, kebijakan ini diuji langsung dengan angka-angka seperti itu, dan pemilik cukup mengganti nilai blok, minimal, dan toleransi di menu Pengaturan.
3. Satu unit, satu sesi: biarkan database yang menjaga
Dua operator yang mengklik unit yang sama hampir bersamaan adalah kasus nyata bila dua komputer memakai file database yang sama. Pengecekan di kode tetap dilakukan agar pesan galatnya ramah, tetapi penjaga terakhirnya adalah partial unique index di SQLite:
CREATE UNIQUE INDEX IF NOT EXISTS ux_unit_main ON sesi(unit_id) WHERE status='MAIN';
Index ini hanya berlaku untuk baris yang sedang main. Riwayat sesi lama di unit yang sama tidak terganggu, tetapi dua sesi berjalan di satu unit mustahil tersimpan. Ditambah transaksi BEGIN IMMEDIATE, tes yang menjalankan delapan thread untuk memulai sesi di unit yang sama selalu menghasilkan tepat satu sesi.
4. Peringatan waktu habis tanpa notifikasi ganda
Setiap detik, layar unit memeriksa sisa waktu semua sesi paket. Peringatan hanya boleh muncul sekali per sesi, jadi id sesi yang sudah diingatkan disimpan di Set:
if (sisa <= 0) {
habis++;
if (sudahHabis.add(c.sesi.id())) {
Toolkit.getDefaultToolkit().beep();
frame.kabar(c.unit.kode() + " waktu habis! (" + c.sesi.pelanggan() + ")");
}
} else if (sisa <= peringatanMenit * 60_000L && sudahDiingatkan.add(c.sesi.id())) {
frame.kabar(c.unit.kode() + " sisa " + ((sisa + 59_999) / 60_000) + " menit");
}
Set.add mengembalikan false bila elemen sudah ada, sehingga satu baris kode sekaligus memeriksa dan mencatat. Warna kartu juga berubah: hijau limau saat main, kuning saat hampir habis, magenta saat habis, dan cyan saat dijeda. Untuk efek berkedip, warna latar label status cukup diganti setiap detik genap, tanpa efek cahaya.
5. Tagihan dihitung ulang saat bayar
Angka biaya di kartu unit hanya tampilan. Saat tombol stop ditekan, service mengambil ulang sesi dari database, menutup jeda yang masih berjalan, lalu menghitung tagihan dengan jam saat itu:
long now = jam.millis();
long jedaTambahan = s.dijeda() ? now - s.jedaSejak() : 0;
Sesi akhir = /* salinan sesi dengan selesai = now dan jedaMs + jedaTambahan */;
Tagihan t = tagihan(akhir, now);
if (diskon > t.total()) throw new Gagal("Diskon melebihi total tagihan.");
long total = t.total() - diskon;
long bayar = metode.equals("TUNAI") ? uang : total;
if (bayar < total) throw new Gagal("Uang kurang " + Rupiah.format(total - bayar) + ".");
Biaya sewa, biaya jajan, diskon, total, dan kembalian semuanya disimpan di baris sesi, ditambah satu baris log BAYAR. Setiap aksi lain (mulai, tambah waktu, jeda, lanjut, pindah, batal) juga masuk log, sehingga pemilik bisa menelusuri apa yang terjadi pada setiap transaksi.
Ringkasan
- Simpan waktu mulai dan total jeda dalam milidetik, lalu hitung lama main dengan rumus; jangan menghitung mundur di memori.
- Pisahkan aturan tarif ke kelas murni agar mudah diuji dan diubah sesuai kebijakan rental.
- Gunakan partial unique index agar satu unit tidak pernah punya dua sesi berjalan.
- Catat sesi yang sudah diperingatkan di
Setsupaya notifikasi tidak berulang. - Hitung ulang tagihan di service saat bayar dan simpan log setiap aksi.
Download source code lengkapnya
Source code lengkapnya berisi layar unit dengan timer, paket dan open billing, tambah waktu, jeda, pindah unit, jajan dengan stok, penjualan jajan langsung, struk thermal, riwayat dengan log, laporan pendapatan dan okupansi, pengaturan tarif, manajemen pengguna, cadangan database, data contoh, dan 15 tes JUnit.
Download Source Code Aplikasi Billing Rental PS Java Swing + SQLite di Papicode
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.
Tutorial lainnya
Cara Membuat Sistem Tiket Travel dengan Pilih Kursi di Google Apps Script: Layout Armada, Anti Kursi Dobel, dan Kursi Kedaluwarsa
20 Oktober 2026
Cara Membuat Website Donasi Transparan dengan Google Apps Script: Kode Unik Transfer dan Laporan Penyaluran
20 Oktober 2026