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:
// 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:
=== 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:
data class Barang(...): lima property, semuaval. Tidak ada property mutable; perubahan stok dilakukan viacopy().val hargaBeli: Long: harga disimpan sebagaiLongagar presisi penuh. Konsisten dengan proyek keuangan sebelumnya.enum class JenisTransaksi { MASUK, KELUAR }: dua nilai.whenatas enum bersifat exhaustif; lupa menangani satu kasus memunculkan peringatan.val catatan: String = "": catatan transaksi opsional. Default string kosong agar tidak perlu selalu diisi.Regex("^[A-Z]+-\\d+$"): pola SKU.^dan$memastikan seluruh string cocok.[A-Z]+satu atau lebih huruf besar,\\d+satu atau lebih digit.b1.copy(stok = b1.stok + 5): membuat instance baru dengan stok ditambah 5. Instance aslib1tetap 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 #
- Kotlin Collections overview
— pengantar koleksi Kotlin untuk
ListdanMutableMapsebagai penyimpanan stok barang
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.