Setiap aplikasi inventaris dimulai dari pertanyaan: apa itu barang, dan bagaimana stok dicatat? Pada Bagian 1 kita merancang dua entitas inti: Barang dengan SKU unik, dan TransaksiStok yang mencatat pergerakan. Validator terpisah memastikan SKU mengikuti format yang konsisten dan stok tidak negatif.

Mengapa SKU, Bukan ID Numerik #

SKU (Stock Keeping Unit) adalah kode unik untuk produk. Format SKU yang baik mudah dibaca manusia, misalnya SKU-001 atau BK-2026-001. Keuntungan dibanding ID numerik:

  • Mudah diucapkan dan diketik manual.
  • Dapat dikodekan dengan kategori (mis. BK- untuk buku).
  • Tidak bergantung pada urutan database.

Pada proyek ini kita memakai format HURUF-ANGKA seperti SKU-001. Validator memastikan format konsisten.

Implementasi Model dan Validator #

Kode berikut mendefinisikan entitas dan validator:

kotlin
// Bagian 1: Model barang dan stok dengan data class.
// Mendemonstrasikan pemodelan barang (SKU, nama, satuan, stok) dan
// transaksi stok masuk/keluar dengan validasi awal.

data class Barang(
    val sku: String,
    val nama: String,
    val satuan: String,
    val hargaBeli: Long,
    val stok: Int
)

enum class JenisTransaksi { MASUK, KELUAR }

data class TransaksiStok(
    val id: String,
    val barangSku: String,
    val jenis: JenisTransaksi,
    val jumlah: Int,
    val catatan: String = ""
)

object ValidatorBarang {
    fun validasiSku(sku: String): Result<String> {
        if (!sku.matches(Regex("^[A-Z]+-\\d+$"))) {
            return Result.failure(IllegalArgumentException("SKU harus format HURUF-ANGKA, mis. ABC-001"))
        }
        return Result.success(sku)
    }

    fun validasiStok(stok: Int): Result<Int> {
        if (stok < 0) return Result.failure(IllegalArgumentException("Stok tidak boleh negatif"))
        return Result.success(stok)
    }

    fun validasiHarga(harga: Long): Result<Long> {
        if (harga <= 0) return Result.failure(IllegalArgumentException("Harga harus positif"))
        return Result.success(harga)
    }
}

fun main() {
    val daftarSku = listOf("SKU-001", "abc-123", "SKU-X")
    println("=== Validasi SKU ===")
    daftarSku.forEach { sku ->
        val r = ValidatorBarang.validasiSku(sku)
        println("SKU \"$sku\": ${r.fold(onSuccess = { "valid" }, onFailure = { it.message })}")
    }

    println()
    println("=== Validasi stok dan harga ===")
    val daftarStok = listOf(10, -5, 0)
    daftarStok.forEach { s ->
        val r = ValidatorBarang.validasiStok(s)
        println("Stok $s: ${r.fold(onSuccess = { "valid" }, onFailure = { it.message })}")
    }
    val daftarHarga = listOf(5_000L, 0L, -100L)
    daftarHarga.forEach { h ->
        val r = ValidatorBarang.validasiHarga(h)
        println("Harga $h: ${r.fold(onSuccess = { "valid" }, onFailure = { it.message })}")
    }

    println()
    println("=== Buat barang valid ===")
    val b1 = Barang("SKU-001", "Buku Kotlin", "pcs", 25_000, 10)
    val b2 = Barang("SKU-002", "Pulpen", "pcs", 3_500, 50)
    listOf(b1, b2).forEach { b ->
        println("[${b.sku}] ${b.nama} ${b.satuan} harga=${b.hargaBeli} stok=${b.stok}")
    }

    println()
    println("=== Buat transaksi ===")
    val t1 = TransaksiStok("T001", b1.sku, JenisTransaksi.MASUK, 5, "Restock awal")
    val t2 = TransaksiStok("T002", b2.sku, JenisTransaksi.KELUAR, 10, "Penjualan")
    listOf(t1, t2).forEach { t ->
        println("[${t.id}] ${t.jenis} ${t.barangSku} x${t.jumlah} (${t.catatan})")
    }

    println()
    println("=== Copy untuk update ===")
    val b1SetelahRestock = b1.copy(stok = b1.stok + 5)
    println("Sebelum: $b1")
    println("Sesudah: $b1SetelahRestock")
}

Output yang Dihasilkan:

text
=== Validasi SKU ===
SKU "SKU-001": valid
SKU "abc-123": SKU harus format HURUF-ANGKA, mis. ABC-001
SKU "SKU-X": SKU harus format HURUF-ANGKA, mis. ABC-001

=== Validasi stok dan harga ===
Stok 10: valid
Stok -5: Stok tidak boleh negatif
Stok 0: valid
Harga 5000: valid
Harga 0: Harga harus positif
Harga -100: Harga harus positif

=== Buat barang valid ===
[SKU-001] Buku Kotlin pcs harga=25000 stok=10
[SKU-002] Pulpen pcs harga=3500 stok=50

=== Buat transaksi ===
[T001] MASUK SKU-001 x5 (Restock awal)
[T002] KELUAR SKU-002 x10 (Penjualan)

=== Copy untuk update ===
Sebelum: Barang(sku=SKU-001, nama=Buku Kotlin, satuan=pcs, hargaBeli=25000, stok=10)
Sesudah: Barang(sku=SKU-001, nama=Buku Kotlin, satuan=pcs, hargaBeli=25000, stok=15)

Mari kita bedah elemen penting:

  1. data class Barang(...): lima property, semua val. Tidak ada property mutable; perubahan stok dilakukan via copy().
  2. val hargaBeli: Long: harga disimpan sebagai Long agar presisi penuh. Konsisten dengan proyek keuangan sebelumnya.
  3. enum class JenisTransaksi { MASUK, KELUAR }: dua nilai. when atas enum bersifat exhaustif; lupa menangani satu kasus memunculkan peringatan.
  4. val catatan: String = "": catatan transaksi opsional. Default string kosong agar tidak perlu selalu diisi.
  5. Regex("^[A-Z]+-\\d+$"): pola SKU. ^ dan $ memastikan seluruh string cocok. [A-Z]+ satu atau lebih huruf besar, \\d+ satu atau lebih digit.
  6. b1.copy(stok = b1.stok + 5): membuat instance baru dengan stok ditambah 5. Instance asli b1 tetap tidak berubah.

Property Mutability #

Seluruh property memakai val. Stok diperbarui dengan copy(), bukan dengan re-assign stok = .... Pendekatan ini disebut immutable domain model dan memberi keuntungan:

  • Aman concurrency: instance tidak berubah, tidak perlu sinkronisasi.
  • Mudah ditelusuri: setiap perubahan menghasilkan instance baru yang dapat dilacak.
  • Prediktabilitas: instance yang sama selalu menghasilkan output yang sama.

Validasi Terpisah #

ValidatorBarang menyatukan tiga fungsi validasi terpisah: SKU, stok, dan harga. Pemisahan ini memberi tiga manfaat:

  • Testabilitas: setiap fungsi dapat diuji sendiri.
  • Reusabilitas: validator dipakai oleh CLI, REST API, dan test.
  • Pesan yang spesifik: pesan galat menyebut field mana yang gagal, bukan “input tidak valid”.

Referensi Lanjutan #

Ringkasan Bagian 1 #

Model Barang dan TransaksiStok kini siap. ValidatorBarang memastikan SKU mengikuti format HURUF-ANGKA, stok tidak negatif, dan harga positif. copy() mendukung perubahan stok tanpa mutasi. Pada Bagian 2 kita membangun logika transaksi masuk dan keluar yang menggunakan model ini.