Ditulis untuk Hafish, founder LabaBersih
Kenapa salah tipe data = uang hilang, data corrupt, atau silent error.
Bayangkan kamu punya rak di gudang. Setiap rak punya label:
Rak "Nama Produk" → bisa diisi tulisan apa aja (string)
Rak "Jumlah Stok" → cuma boleh angka bulat (integer)
Rak "Harga" → angka desimal, presisi rupiah (decimal)
Rak "Aktif?" → cuma boleh Ya atau Tidak (boolean)
Rak "Milik Produk" → harus nunjuk ke produk yang BENERAN ADA (FK/UUID)
Kalau kamu taruh tulisan "sepuluh" di rak "Jumlah Stok" — sistem error. Itu tipe data.
Tipe data = aturan tentang ISI APA yang boleh masuk ke setiap kolom database.
Untuk: nama, email, status, SKU, kode.
product.name = "Forbest Ayam 1kg"
product.sku = "FB-AYM-1KG"
order.status = "dikirim"
Untuk: alamat, catatan, deskripsi.
order.customer_address = "Jl. Raya Kediri No. 123 RT 01/RW 02 Kec. Mojoroto..."
journal.description = "Penjualan TikTok Bestari ID — 31 Maret 2026, 285 order"
Untuk: jumlah barang, quantity.
product.stok = 100
order_item.quantity = 3
product.reserved_stock = 15
Untuk: harga, total, fee, HPP — semua yang berhubungan dengan UANG.
product.harga_modal = Decimal.new("45000") # Rp 45.000
order.total_harga = Decimal.new("200000") # Rp 200.000
lot.cost_per_unit = Decimal.new("47500") # Rp 47.500
0.1 + 0.2 = 0.30000000000000004
Untuk: aktif/nonaktif, sudah/belum.
product.is_active = true
entity.is_default = true
store.is_active = false
nil (bukan true/bukan false).
if product.is_active do → nil = false. Tapi if !product.is_active do → nil = true!
is_active, :boolean, default: true
Ini yang PALING PENTING dan paling sering bikin masalah.
# UUID = ID unik yang di-generate otomatis
product.id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
# FK = Foreign Key = "kolom ini HARUS nunjuk ke data yang ada di tabel lain"
order.store_id = "b2c3d4e5-f6a7-8901-bcde-f12345678901"
# ↑ ini HARUS ada di tabel stores
FK itu kayak link yang dijamin valid. Beda sama string:
# String (v1): simpan kode akun sebagai text
store.piutang_account_code = "12-201"
# Database gak tau apakah "12-201" beneran ada
# Kalau salah ketik "12-2O1" (huruf O bukan nol) → SILENT ERROR
# FK (v2): simpan ID akun sebagai reference
store.piutang_account_id = "uuid-of-actual-account"
# Database GUARANTEE: ID ini PASTI ada di tabel accounts
# Kalau coba simpan ID yang gak ada → ERROR LANGSUNG (bukan silent)
Ini kasus nyata dari LabaBersih:
LANGKAH:
1. Ambil akun piutang dari store
2. Bikin jurnal: Dr. Piutang / Cr. Pendapatan
3. Order status → dikirim
# Store punya piutang_account_code = "12-201"
account = cari_akun_by_code("12-201")
MASALAH 1: Kode "12-201" gak ada (salah input)
→ account = nil
→ Jurnal SKIP (gak dibuat)
→ Order tetap dikirim
→ Revenue HILANG dari laporan keuangan
→ Hafish gak tau sampai akhir bulan
MASALAH 2: Akun "12-201" dihapus
→ Kode masih tersimpan di store sebagai string
→ Ship order → cari "12-201" → gak ketemu → nil → skip
→ Silent error LAGI
MASALAH 3: Kode akun diganti dari "12-201" ke "12-2-101"
→ Store masih simpan "12-201" (string lama)
→ Gak pernah ketemu lagi
# Store punya piutang_account_id = UUID
account = ambil_akun_by_id(store.piutang_account_id)
MASALAH 1: ID gak ada?
→ TIDAK MUNGKIN. Database BLOCK insert kalau ID gak valid.
→ Error langsung saat bikin store, bukan saat ship order.
MASALAH 2: Akun dihapus?
→ Database BLOCK delete (on_delete: :restrict)
→ "Gak bisa hapus akun karena masih dipakai oleh store"
MASALAH 3: Kode akun diganti?
→ Gak masalah. FK pakai ID, bukan kode. ID gak pernah berubah.
Race condition = 2 proses jalan bersamaan, hasilnya kacau.
Stok Forbest = 10. Dua orang ship bersamaan.
Waktu | Proses A (ship 8) | Proses B (ship 5)
---------|--------------------------|---------------------------
00:01 | Baca stok = 10 |
00:01 | | Baca stok = 10 (SAMA!)
00:02 | 10 - 8 = 2, OK |
00:02 | | 10 - 5 = 5, OK (harusnya GAGAL!)
00:03 | UPDATE stok = 2 |
00:03 | | UPDATE stok = 5
---------|--------------------------|---------------------------
Hasil: stok = 5 (terakhir yang UPDATE menang)
Padahal: 8 + 5 = 13 barang terjual dari stok 10!
→ OVERSELL. Barang gak ada tapi order jalan.
Waktu | Proses A (ship 8) | Proses B (ship 5)
---------|--------------------------|---------------------------
00:01 | LOCK product row |
00:01 | | Mau LOCK → ANTRI (tunggu A selesai)
00:02 | Baca stok = 10 |
00:03 | 10 - 8 = 2, OK |
00:04 | UPDATE stok = 2 |
00:05 | COMMIT → RELEASE LOCK |
00:05 | | LOCK acquired!
00:06 | | Baca stok = 2 (FRESH, bukan 10!)
00:07 | | 2 - 5 = -3 → TOLAK!
---------|--------------------------|---------------------------
Hasil: stok = 2, proses B ditolak. BENAR.
| Data | Tipe | Kenapa |
|---|---|---|
| Nama produk | String | Pendek, bisa di-search |
| Alamat | Text | Bisa sangat panjang |
| Harga, total, fee | Decimal | Uang harus presisi |
| Quantity, stok | Integer | Gak ada 0.5 barang |
| Aktif/nonaktif | Boolean | Cuma 2 nilai |
| Status | String + validasi | Harus dari list (dikirim/selesai/rts), bukan free text |
| No. HP | String | "08123" → integer hilang leading zero |
| NPWP | String | "01.234.567.8-901.000" ada titik dan strip |
| Tanggal | Date / DateTime | Bisa compare, sort, timezone-aware |
| ID entity | UUID (binary_id) | Unik, gak bisa ditebak |
| Referensi ke entity lain | FK (UUID) | Database jamin data ada |
| Referensi ke akun (v1) | String "12-201" | SALAH — silent error |
| Referensi ke akun (v2) | FK (UUID) | BENAR — database enforce |
| Fee mapping | JSONB (array of maps) | Flexible, jumlah fee beda per toko |
Kalau kamu ketemu masalah data di LabaBersih, tanya seperti ini:
TEMPLATE:
"Di [halaman/fitur apa], saya [melakukan apa],
tapi hasilnya [apa yang salah].
Kode/field yang terlibat: [nama field/tabel].
Error message (kalau ada): [copy paste].
Saya pikir masalahnya di [tebakan kamu]."
Contoh bagus:
"Di halaman pesanan, saya ship order, tapi jurnal gak muncul.
Field: store.piutang_account_code = '12-201'.
Error: gak ada error, tapi jurnal kosong.
Saya pikir kode akun '12-201' gak ketemu di database."
Ini lebih baik dari: "Ship order error, kenapa?"