Setiap aplikasi keuangan dimulai dari satu kemampuan dasar: mencatat transaksi. Pada Bagian 1 kita membangun struktur transaksi dengan dua jenis (pemasukan dan pengeluaran), menghitung saldo, dan memformat nominal dalam Rupiah dengan pemisah ribuan. Fondasi ini akan dipakai untuk laporan dan anggaran di Bagian berikutnya.

Enum untuk Jenis Transaksi #

Hanya dua kemungkinan jenis transaksi: pemasukan atau pengeluaran. enum class cocok karena nilainya terbatas dan when atas enum bersifat exhaustif. Compiler memaksa kita menangani setiap kasus, sehingga tidak ada jenis transaksi yang terlewat.

Implementasi Pencatatan #

Kode berikut menunjukkan class BukuKeuangan yang menyimpan daftar transaksi dan menghitung saldo:

kotlin
// Bagian 1: Pencatatan transaksi pemasukan/pengeluaran.
// Mendemonstrasikan enum jenis transaksi, data class Transaksi, dan
// perhitungan saldo sederhana.

enum class JenisTransaksi { PEMASUKAN, PENGELUARAN }

data class Transaksi(
    val id: String,
    val jenis: JenisTransaksi,
    val jumlah: Long,
    val tanggal: String,
    val catatan: String = ""
)

class BukuKeuangan {
    private val daftar = mutableListOf<Transaksi>()
    private var urutan = 0

    fun tambah(jenis: JenisTransaksi, jumlah: Long, tanggal: String, catatan: String = ""): Transaksi {
        require(jumlah > 0) { "Jumlah harus positif" }
        require(jumlah <= 1_000_000_000L) { "Jumlah melebihi batas wajar" }
        urutan += 1
        val t = Transaksi("T%04d".format(urutan), jenis, jumlah, tanggal, catatan)
        daftar.add(t)
        return t
    }

    fun semua(): List<Transaksi> = daftar.toList()

    fun totalPemasukan(): Long = daftar
        .filter { it.jenis == JenisTransaksi.PEMASUKAN }
        .sumOf { it.jumlah }

    fun totalPengeluaran(): Long = daftar
        .filter { it.jenis == JenisTransaksi.PENGELUARAN }
        .sumOf { it.jumlah }

    fun saldo(): Long = totalPemasukan() - totalPengeluaran()
}

fun formatRupiah(nilai: Long): String {
    val str = nilai.toString()
    val denganTitik = str.reversed().chunked(3).joinToString(".").reversed()
    return if (nilai < 0) "-Rp$denganTitik" else "Rp$denganTitik"
}

fun main() {
    val buku = BukuKeuangan()
    buku.tambah(JenisTransaksi.PEMASUKAN, 5_000_000, "2026-01-05", "Gaji bulanan")
    buku.tambah(JenisTransaksi.PENGELUARAN, 750_000, "2026-01-08", "Belanja bulanan")
    buku.tambah(JenisTransaksi.PENGELUARAN, 1_200_000, "2026-01-12", "Transportasi")
    buku.tambah(JenisTransaksi.PEMASUKAN, 1_500_000, "2026-01-15", "Proyek sampingan")
    buku.tambah(JenisTransaksi.PENGELUARAN, 500_000, "2026-01-20", "Hiburan")

    println("=== Daftar Transaksi ===")
    buku.semua().forEach { t ->
        val arah = if (t.jenis == JenisTransaksi.PEMASUKAN) "+" else "-"
        println("[${t.id}] $arah ${formatRupiah(t.jumlah)} ${t.tanggal} - ${t.catatan}")
    }
    println()
    println("Total pemasukan : ${formatRupiah(buku.totalPemasukan())}")
    println("Total pengeluaran: ${formatRupiah(buku.totalPengeluaran())}")
    println("Saldo akhir     : ${formatRupiah(buku.saldo())}")
    println()
    println("=== Validasi gagal ===")
    try {
        buku.tambah(JenisTransaksi.PENGELUARAN, -100, "2026-01-22")
    } catch (e: IllegalArgumentException) {
        println("Galat: ${e.message}")
    }
}

Output yang Dihasilkan:

text
=== Daftar Transaksi ===
[T0001] + Rp5.000.000 2026-01-05 - Gaji bulanan
[T0002] - Rp750.000 2026-01-08 - Belanja bulanan
[T0003] - Rp1.200.000 2026-01-12 - Transportasi
[T0004] + Rp1.500.000 2026-01-15 - Proyek sampingan
[T0005] - Rp500.000 2026-01-20 - Hiburan

Total pemasukan : Rp6.500.000
Total pengeluaran: Rp2.450.000
Saldo akhir     : Rp4.050.000

=== Validasi gagal ===
Galat: Jumlah harus positif

Mari kita bedah elemen penting:

  1. enum class JenisTransaksi { PEMASUKAN, PENGELUARAN }: dua konstanta. when (t.jenis) akan exhaustif, sehingga lupa menangani satu kasus memunculkan peringatan compiler.
  2. require(jumlah > 0) { ... }: preconditon yang melempar IllegalArgumentException jika gagal. Cocok untuk input yang tidak valid menandakan bug.
  3. require(jumlah <= 1_000_000_000L): batas wajar satu miliar. Mencegah typo yang menambahkan terlalu banyak nol.
  4. "T%04d".format(urutan): format ID dengan empat digit, misalnya T0001. Konsistensi format penting untuk pelaporan dan pengurutan.
  5. daftar.filter { ... }.sumOf { it.jumlah }: kombinator koleksi. filter memilih transaksi sesuai jenis, sumOf menjumlahkan field jumlah. Pola ini muncul berulang di Bagian 2 dan 3.
  6. str.reversed().chunked(3).joinToString(".").reversed(): trik format ribuan. String dibalik, dipotong tiga karakter, digabung dengan titik, lalu dibalik lagi. Hasilnya “5.000.000” untuk 5 juta.

Long, Bukan Double, untuk Uang #

Tipe Long (64-bit integer) menyimpan nominal hingga 9,2 triliun. Double tidak cocok karena tidak dapat menyimpan desimal secara presisi: 0.1 + 0.2 di Double menghasilkan 0.30000000000000004. Untuk aplikasi keuangan nyata, BigDecimal lebih tepat jika butuh pecahan sen; untuk belajar, Long dalam satuan Rupiah penuh sudah memadai.

Format Rupiah #

Fungsi formatRupiah(nilai: Long) mengubah angka ke format yang mudah dibaca. Trik reversed().chunked(3) adalah cara idiomatik Kotlin untuk menambah pemisah ribuan. Untuk produksi, java.text.DecimalFormat lebih robust karena mendukung lokal, tetapi untuk belajar fungsi sederhana ini cukup.

Referensi Lanjutan #

  • Kotlin Collections overview — pengantar koleksi Kotlin untuk List, filter, dan sumOf yang dipakai pada pencatatan transaksi
  • Kotlin control flow — rujukan when dan if expression untuk memilah jenis transaksi pemasukan dan pengeluaran

Ringkasan Bagian 1 #

Pencatatan transaksi kini berfungsi: tambah() menyimpan transaksi dengan ID otomatis, saldo() menjumlahkan pemasukan dikurangi pengeluaran, dan formatRupiah() menampilkan nominal dengan pemisah ribuan. Validasi require() menolak jumlah negatif dan di atas batas wajar. Pada Bagian 2 kita menambahkan kategori dan anggaran bulanan untuk melacak pengeluaran per kategori.