Model di Bagian 1 belum menggambarkan bagaimana stok berubah. Pada Bagian 2 kita mengimplementasikan dua operasi transaksi: masuk (stok bertambah) dan keluar (stok berkurang). Setiap transaksi dicatat dengan stok sebelum dan sesudah, sehingga riwayat dapat merekonstruksi perubahan stok dari waktu ke waktu.
Aturan Bisnis Transaksi #
Empat aturan yang harus dipenuhi:
- Jumlah transaksi harus positif (tidak boleh nol atau negatif).
- Barang harus terdaftar sebelum transaksi.
- Transaksi
KELUARmenolak jika stok tidak cukup. - Setiap transaksi menghasilkan ID dengan prefix
M(masuk) atauK(keluar).
Pelanggaran aturan mengembalikan Result.failure dengan pesan yang informatif. Tidak ada exception yang dilempar; pemanggil memutuskan cara menangani.
Implementasi Class Inventaris #
Kode berikut menunjukkan implementasi transaksi dengan riwayat stok sebelum/sesudah:
// Bagian 2: Transaksi barang masuk dan keluar.
// Mendemonstrasikan logika bisnis transaksi: stok bertambah untuk MASUK,
// berkurang untuk KELUAR, dengan validasi stok tidak negatif.
data class Barang(
val sku: String,
val nama: String,
val stok: Int
)
enum class JenisTransaksi { MASUK, KELUAR }
data class TransaksiStok(
val id: String,
val barangSku: String,
val jenis: JenisTransaksi,
val jumlah: Int,
val stokSebelum: Int,
val stokSesudah: Int
)
class Inventaris {
private val barang = mutableMapOf<String, Barang>()
private val transaksi = mutableListOf<TransaksiStok>()
private var urutan = 0
fun daftarkan(b: Barang) { barang[b.sku] = b }
fun catatMasuk(sku: String, jumlah: Int, catatan: String = ""): Result<TransaksiStok> {
if (jumlah <= 0) return Result.failure(IllegalArgumentException("Jumlah harus positif"))
val b = barang[sku] ?: return Result.failure(IllegalStateException("Barang tidak ditemukan"))
urutan += 1
val t = TransaksiStok(
id = "M%04d".format(urutan),
barangSku = sku,
jenis = JenisTransaksi.MASUK,
jumlah = jumlah,
stokSebelum = b.stok,
stokSesudah = b.stok + jumlah
)
barang[sku] = b.copy(stok = b.stok + jumlah)
transaksi.add(t)
return Result.success(t)
}
fun catatKeluar(sku: String, jumlah: Int, catatan: String = ""): Result<TransaksiStok> {
if (jumlah <= 0) return Result.failure(IllegalArgumentException("Jumlah harus positif"))
val b = barang[sku] ?: return Result.failure(IllegalStateException("Barang tidak ditemukan"))
if (b.stok < jumlah) {
return Result.failure(IllegalStateException("Stok ${b.nama} tidak cukup (${b.stok} < $jumlah)"))
}
urutan += 1
val t = TransaksiStok(
id = "K%04d".format(urutan),
barangSku = sku,
jenis = JenisTransaksi.KELUAR,
jumlah = jumlah,
stokSebelum = b.stok,
stokSesudah = b.stok - jumlah
)
barang[sku] = b.copy(stok = b.stok - jumlah)
transaksi.add(t)
return Result.success(t)
}
fun stok(sku: String): Int = barang[sku]?.stok ?: 0
fun semuaBarang(): List<Barang> = barang.values.toList().sortedBy { it.sku }
fun riwayatTransaksi(sku: String? = null): List<TransaksiStok> {
return if (sku == null) transaksi.toList() else transaksi.filter { it.barangSku == sku }
}
}
fun main() {
val inv = Inventaris()
inv.daftarkan(Barang("SKU-001", "Buku Kotlin", 10))
inv.daftarkan(Barang("SKU-002", "Pulpen", 50))
println("=== Transaksi Masuk ===")
inv.catatMasuk("SKU-001", 5).onSuccess { t ->
println("[${t.id}] +${t.jumlah} ${t.barangSku}: ${t.stokSebelum} -> ${t.stokSesudah}")
}
println()
println("=== Transaksi Keluar ===")
inv.catatKeluar("SKU-002", 20).onSuccess { t ->
println("[${t.id}] -${t.jumlah} ${t.barangSku}: ${t.stokSebelum} -> ${t.stokSesudah}")
}
println()
println("=== Stok Tidak Cukup ===")
val gagal = inv.catatKeluar("SKU-001", 100)
println("Hasil: ${gagal.fold(onSuccess = { "berhasil?!" }, onFailure = { it.message })}")
println()
println("=== Riwayat Transaksi SKU-001 ===")
inv.riwayatTransaksi("SKU-001").forEach { t ->
val arah = if (t.jenis == JenisTransaksi.MASUK) "+" else "-"
println("[${t.id}] $arah${t.jumlah}: ${t.stokSebelum} -> ${t.stokSesudah}")
}
println()
println("=== Stok Akhir ===")
inv.semuaBarang().forEach { println("[${it.sku}] ${it.nama} stok=${it.stok}") }
}Output yang Dihasilkan:
=== Transaksi Masuk ===
[M0001] +5 SKU-001: 10 -> 15
=== Transaksi Keluar ===
[K0002] -20 SKU-002: 50 -> 30
=== Stok Tidak Cukup ===
Hasil: Stok Buku Kotlin tidak cukup (15 < 100)
=== Riwayat Transaksi SKU-001 ===
[M0001] +5: 10 -> 15
=== Stok Akhir ===
[SKU-001] Buku Kotlin stok=15
[SKU-002] Pulpen stok=30Mari kita bedah logika transaksi:
if (jumlah <= 0) return Result.failure(...): validasi jumlah. Nol atau negatif langsung ditolak sebelum memeriksa barang. Pesan galat spesifik menyebut masalahnya.val b = barang[sku] ?: return Result.failure(...): operator Elvis memilih antara barang yang ditemukan atau mengembalikan kegagalan. Tidak ada exception; pemanggil memutuskan cara menangani.if (b.stok < jumlah): validasi stok cukup. Pesan galat menyertakan stok saat ini dan jumlah diminta, sehingga pengguna tahu seberapa jauh kekurangan."M%04d".format(urutan): format ID dengan prefixM(masuk) dan empat digit.K%04duntuk keluar. Prefix membantu identifikasi jenis transaksi tanpa membaca detail.stokSebelum = b.stok, stokSesudah = b.stok + jumlah: snapshot stok sebelum dan sesudah. Riwayat dapat merekonstruksi perubahan tanpa menghitung ulang seluruh transaksi.barang[sku] = b.copy(stok = b.stok + jumlah): mengganti entri Map dengan instance barang baru. Instance lama tetap ada dalamTransaksiStok.stokSebelum.
Event Sourcing Ringan #
Pola mencatat stokSebelum dan stokSesudah adalah bentuk ringan event sourcing. Daripada hanya menyimpan stok akhir, kita menyimpan setiap perubahan. Manfaat:
- Audit: riwayat dapat ditelusuri untuk setiap transaksi.
- Rekonstruksi: stok dapat dihitung ulang dari transaksi awal.
- Rollback: transaksi yang salah dapat dibatalkan dengan reverse operation.
Kekurangan: ruang penyimpanan tumbuh seiring waktu. Untuk skala besar, lakukan snapshot periodik dan arsip transaksi lama.
ID Transaksi dengan Prefix #
ID dengan prefix M/K lebih informatif daripada numerik murni:
M0001jelas adalah transaksi masuk.K0002jelas adalah transaksi keluar.
Saat debugging log, prefix memudahkan pencarian tanpa membaca detail. Format yang konsisten juga memudahkan filtering dan analisis.
Riwayat Transaksi #
Method riwayatTransaksi(sku: String? = null) mengembalikan transaksi, dengan opsi filter berdasarkan SKU. Jika sku null, kembalikan semua transaksi. Filter di sisi server mengurangi data yang dikirim ke klien, berguna untuk UI yang menampilkan riwayat satu barang.
Referensi Lanjutan #
- Kotlin Collections overview
— pengantar koleksi Kotlin untuk
MapdanArrayDequepada transaksi masuk-keluar barang
Ringkasan Bagian 2 #
Transaksi masuk dan keluar kini berfungsi: catatMasuk() menambah stok, catatKeluar() mengurangi stok dengan validasi. Setiap transaksi mencatat stokSebelum dan stokSesudah untuk audit. ID dengan prefix M/K memudahkan identifikasi jenis. Pada Bagian 3 kita menambahkan database SQLite untuk persistensi dan pencarian barang.