363 lines
15 KiB
Markdown
363 lines
15 KiB
Markdown
# RFC: Produk Timbangan (Weight-Based Products)
|
||||
|
|
|
|||
|
|
**Status:** Diimplementasikan (migrasi `000089`)
|
|||
|
|
**Tanggal:** 2026-09-05, diperbarui 2026-09-06
|
|||
|
|
**Scope:** Product, Order, Void/Refund, Report
|
|||
|
|
**Out of scope:** Inventory / pengurangan stok otomatis (lihat §8)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Masalah
|
|||
|
|
|
|||
|
|
Sistem mengasumsikan setiap produk dijual dalam satuan diskrit. `order_items.quantity`
|
|||
|
|
bertipe `INTEGER` dengan `CHECK (quantity > 0)`, dan harga dihitung
|
|||
|
|
`quantity × unit_price` di seluruh jalur order, void, refund, dan split bill.
|
|||
|
|
|
|||
|
|
Produk seperti Ikan Tude dijual per timbangan. Pelanggan memesan Ikan Tude 4,2 ons,
|
|||
|
|
lalu memesan Ikan Tude lagi 5,6 ons. Keduanya adalah **dua ikan berbeda yang ditimbang
|
|||
|
|
terpisah** — bukan satu baris berisi 9,8.
|
|||
|
|
|
|||
|
|
Angka 4,2 itu **berat**, bukan cacah. Sistem belum punya tempat untuk menyimpannya.
|
|||
|
|
|
|||
|
|
> **Catatan satuan.** RFC ini tidak mengasumsikan satuan tertentu. Satuan produk
|
|||
|
|
> ditentukan `products.unit_id` yang merujuk tabel `units` — bisa ons, kg, gram, atau
|
|||
|
|
> apa pun yang didefinisikan organisasi. Contoh memakai **ons** karena itu kasus yang
|
|||
|
|
> sedang dikerjakan; tidak ada bagian desain ini yang bergantung padanya.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Keputusan Inti
|
|||
|
|
|
|||
|
|
**Satu penimbangan = satu baris `order_items`.**
|
|||
|
|
|
|||
|
|
| | Baris 1 | Baris 2 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| Ikan Tude 4,2 ons | `quantity = 1`, `weight = 4.2` | |
|
|||
|
|
| Ikan Tude 5,6 ons | | `quantity = 1`, `weight = 5.6` |
|
|||
|
|
|
|||
|
|
`quantity` tetap `INTEGER` dan tetap berarti "berapa banyak barang". Berat masuk ke
|
|||
|
|
kolom baru. Dua baris tidak pernah digabung menjadi `9.8`, karena keduanya memang dua
|
|||
|
|
ikan yang berbeda.
|
|||
|
|
|
|||
|
|
### Kenapa bukan `quantity = 4.2`
|
|||
|
|
|
|||
|
|
Alternatif yang sempat dipertimbangkan adalah mengubah `quantity` menjadi
|
|||
|
|
`DECIMAL(12,3)`. Model itu ditolak karena tiga alasan:
|
|||
|
|
|
|||
|
|
1. **Menghapus jejak barang.** `4.2` dan `5.6` yang digabung jadi `9.8` kehilangan
|
|||
|
|
informasi bahwa ada dua ikan. Tidak bisa direkonstruksi.
|
|||
|
|
2. **Merusak agregasi lintas produk.** `SUM(quantity)` untuk laporan
|
|||
|
|
"total item terjual" akan menjumlahkan ons dengan porsi — angka tanpa arti, yang
|
|||
|
|
bahkan berubah nilainya bila satuan produk diganti dari ons ke kg tanpa ada apa pun
|
|||
|
|
yang berubah di dunia nyata.
|
|||
|
|
3. **Membawa masalah presisi float ke seluruh sistem.** Perbandingan quantity dipakai
|
|||
|
|
di void, refund, dan split bill. Dengan float, `1,4 + 1,4 + 1,4` tidak sama dengan
|
|||
|
|
`4,2` — split bill "bagi rata bertiga" akan gagal menandai item lunas meski uang
|
|||
|
|
sudah diterima penuh. Semua itu tidak terjadi bila `quantity` tetap integer.
|
|||
|
|
|
|||
|
|
Konsekuensi langsung dari keputusan ini: **tidak diperlukan helper perbandingan
|
|||
|
|
epsilon.** Berat tidak pernah dibandingkan, hanya dikalikan.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Prinsip
|
|||
|
|
|
|||
|
|
**P1 — Baris transaksi adalah snapshot yang beku.**
|
|||
|
|
`order_items` sudah menyimpan `unit_price` dan `unit_cost` sebagai salinan, bukan join
|
|||
|
|
ke `products`. Satuan mendapat perlakuan sama: mengubah master data tidak boleh
|
|||
|
|
mengubah arti transaksi yang sudah terjadi.
|
|||
|
|
|
|||
|
|
**P2 — Perhitungan harga baris hanya ada di satu tempat.**
|
|||
|
|
Setelah RFC ini ada dua rumus (`quantity × harga` dan `weight × harga`). Tidak boleh
|
|||
|
|
ada perkalian harga yang tersebar; semuanya memanggil satu fungsi.
|
|||
|
|
|
|||
|
|
**P3 — Harga tetap otoritas backend.**
|
|||
|
|
Klien tidak pernah mengirim harga. Backend membacanya dari `products` /
|
|||
|
|
`product_outlet_prices` seperti sekarang.
|
|||
|
|
|
|||
|
|
**P4 — Berat boleh dijumlahkan dalam satu produk, tidak boleh antar produk.**
|
|||
|
|
`SUM(weight)` untuk satu produk bermakna ("terjual 47,3 ons"). Lintas produk dengan
|
|||
|
|
satuan berbeda tidak bermakna.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Perubahan Skema
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- Products: cara jual
|
|||
|
|
ALTER TABLE products
|
|||
|
|
ADD COLUMN sell_by VARCHAR(20) NOT NULL DEFAULT 'unit'
|
|||
|
|
CHECK (sell_by IN ('unit', 'weight'));
|
|||
|
|
|
|||
|
|
-- Order items: berat + snapshot satuan
|
|||
|
|
ALTER TABLE order_items
|
|||
|
|
ADD COLUMN weight DECIMAL(12,3),
|
|||
|
|
ADD COLUMN unit_id UUID REFERENCES units(id) ON DELETE RESTRICT;
|
|||
|
|
|
|||
|
|
ALTER TABLE order_items
|
|||
|
|
ADD CONSTRAINT chk_order_items_weight_positive
|
|||
|
|
CHECK (weight IS NULL OR weight > 0),
|
|||
|
|
ADD CONSTRAINT chk_order_items_weight_single_line
|
|||
|
|
CHECK (weight IS NULL OR quantity = 1);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Catatan:**
|
|||
|
|
|
|||
|
|
- `weight` **nullable**. `NULL` berarti produk satuan biasa — seluruh data lama valid
|
|||
|
|
tanpa backfill, dan perilakunya tidak berubah sama sekali.
|
|||
|
|
- `chk_order_items_weight_single_line` menegakkan keputusan §2 di level database:
|
|||
|
|
baris berbobot selalu `quantity = 1`. Ini yang membuat `BillableQuantity()` tidak
|
|||
|
|
ambigu dan membuat void otomatis bersifat utuh (§6).
|
|||
|
|
- `quantity` **tidak berubah tipe**. `CHECK (quantity > 0)` yang sudah ada tetap
|
|||
|
|
berlaku.
|
|||
|
|
- `DECIMAL(12,3)` konsisten dengan `inventory_movements.quantity` yang sudah memakai
|
|||
|
|
presisi sama.
|
|||
|
|
- Tidak ada `weighed_unit`. Karena satu baris memang satu barang, "ikan curah" dan
|
|||
|
|
"ikan per ekor" berperilaku identik — pembedaan itu tidak punya konsekuensi.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Perhitungan Harga
|
|||
|
|
|
|||
|
|
Satu-satunya tempat yang boleh mengalikan harga (P2):
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// BillableQuantity mengembalikan pengali harga untuk baris ini:
|
|||
|
|
// berat bila produk dijual per timbangan, jumlah bila dijual per satuan.
|
|||
|
|
// Baris berbobot dijamin quantity = 1 oleh constraint DB.
|
|||
|
|
func (oi *OrderItem) BillableQuantity() float64 {
|
|||
|
|
if oi.Weight != nil {
|
|||
|
|
return *oi.Weight
|
|||
|
|
}
|
|||
|
|
return float64(oi.Quantity)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (oi *OrderItem) CalculateTotalPrice() {
|
|||
|
|
oi.TotalPrice = RoundMoney(oi.BillableQuantity() * oi.UnitPrice)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (oi *OrderItem) CalculateTotalCost() {
|
|||
|
|
oi.TotalCost = RoundMoney(oi.BillableQuantity() * oi.UnitCost)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`unit_price` tetap berarti **harga per satu satuan produk** (per ons). Tidak ada faktor
|
|||
|
|
konversi yang menyelinap ke perhitungan uang.
|
|||
|
|
|
|||
|
|
### Titik yang harus diganti
|
|||
|
|
|
|||
|
|
Ini bagian paling berisiko dari RFC. Setiap perkalian harga yang terlewat akan
|
|||
|
|
menghitung `1 × harga_per_ons` — ikan 4,2 ons ditagih seharga 1 ons. **Salah uang,
|
|||
|
|
bukan salah tampilan**, dan tidak memicu error apa pun.
|
|||
|
|
|
|||
|
|
| Lokasi | Sekarang |
|
|||
|
|
|---|---|
|
|||
|
|
| `processor/order_processor.go:197-198` | buat order |
|
|||
|
|
| `processor/order_processor.go:330-331` | tambah item ke order |
|
|||
|
|
| `processor/order_processor.go:605-606` | jumlah & HPP yang di-void |
|
|||
|
|
| `processor/order_processor.go:723` | jumlah refund |
|
|||
|
|
| `processor/split_bill_processor.go:143` | hitung jumlah split |
|
|||
|
|
| `processor/split_bill_processor.go:189` | catat pembayaran |
|
|||
|
|
| `processor/split_bill_processor.go:231` | metadata pembayaran |
|
|||
|
|
| `repository/order_item_repository.go:113` | jumlah void penuh |
|
|||
|
|
|
|||
|
|
Implementasi menemukan **lima titik tambahan** di luar daftar di atas, semuanya di jalur
|
|||
|
|
inventory movement dan resep bahan yang tidak terlihat saat RFC ini ditulis:
|
|||
|
|
|
|||
|
|
| Lokasi | Status |
|
|||
|
|
|---|---|
|
|||
|
|
| `order_processor.go:1056` `createInventoryMovement` | mati (0 pemanggil), tetap diperbaiki |
|
|||
|
|
| `order_processor.go:1354` `prepareProductInventoryMovement` | **hidup** |
|
|||
|
|
| `order_processor.go:1420` `prepareIngredientRecipeItem` | **hidup** |
|
|||
|
|
| `order_processor.go:1518` `prepareRefundProductInventoryMovement` | mati (0 pemanggil), tetap diperbaiki |
|
|||
|
|
| `order_processor.go:1584` `prepareRefundedIngredientRecipeItem` | **hidup** |
|
|||
|
|
|
|||
|
|
Tiga yang hidup penting: tanpa perbaikan, konsumsi bahan untuk ikan 4,2 ons akan
|
|||
|
|
dihitung sebagai 1 satuan resep.
|
|||
|
|
|
|||
|
|
### Verifikasi
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
grep -rn "Quantity) \* \|Quantity \* " --include=*.go internal/ \
|
|||
|
|
| grep -iE "price|cost" | grep -v BillableQuantity | grep -v totalIngredientQuantity
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Hasilnya **tidak kosong** — tersisa tujuh baris, semuanya sudah diperiksa dan aman:
|
|||
|
|
|
|||
|
|
- `mappers/inventory_movement_mapper.go:129` dan
|
|||
|
|
`processor/inventory_movement_processor.go:69` — penyesuaian stok manual, bukan
|
|||
|
|
baris order.
|
|||
|
|
- `repository/order_item_repository.go:144,146,147,165,166` — cabang void sebagian,
|
|||
|
|
yang baris berbobot tidak pernah jangkau karena dijaga `orderItem.IsWeighed()`.
|
|||
|
|
|
|||
|
|
Bila daftar ini bertambah di kemudian hari, baris barunya harus diperiksa satu per satu.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Void, Refund, Split Bill
|
|||
|
|
|
|||
|
|
**Tidak ada perubahan logika.** Ini konsekuensi menyenangkan dari `quantity` yang tetap
|
|||
|
|
integer.
|
|||
|
|
|
|||
|
|
**Void.** `VoidOrderItem` (`repository/order_item_repository.go:104`) bercabang pada
|
|||
|
|
`voidQuantity >= orderItem.Quantity`. Untuk baris berbobot, `quantity` dijamin `1` dan
|
|||
|
|
`voidQuantity` minimal `1`, sehingga **selalu** masuk cabang void penuh. Cabang
|
|||
|
|
pemecahan baris tidak pernah tersentuh, sehingga tidak mungkin lahir baris sisa
|
|||
|
|
berbobot nol. Yang berubah hanya perhitungan `voidedAmount` di baris 113 (§5).
|
|||
|
|
|
|||
|
|
**Refund.** Sama — refund baris berbobot bersifat utuh. Hanya `refundAmount` di
|
|||
|
|
`order_processor.go:723` yang perlu memakai `BillableQuantity()`.
|
|||
|
|
|
|||
|
|
**Split bill.** `payment_order_items.quantity` tetap `INTEGER`. Untuk baris berbobot
|
|||
|
|
nilainya `0` atau `1` — bayar penuh atau tidak sama sekali. Seluruh perbandingan di
|
|||
|
|
`split_bill_processor.go` tetap aritmatika bilangan bulat, sehingga masalah presisi
|
|||
|
|
float tidak pernah muncul. Hanya perhitungan `itemAmount` (baris 143 dan 189) yang
|
|||
|
|
berubah.
|
|||
|
|
|
|||
|
|
**Batasan yang diterima:** refund atau void **sebagian berat** (mengembalikan 1 ons
|
|||
|
|
dari baris 4,2 ons) tidak didukung. Untuk barang yang sudah ditimbang dan diserahkan,
|
|||
|
|
koreksi sebagian pada praktiknya berarti salah timbang — yang penanganan benarnya
|
|||
|
|
adalah void baris itu lalu input ulang, bukan mengubah berat baris yang sudah tercatat.
|
|||
|
|
Ini menjaga jejak audit tetap jujur.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. Validasi & Tampilan
|
|||
|
|
|
|||
|
|
### 7.1 Aturan validasi
|
|||
|
|
|
|||
|
|
Divalidasi di processor saat membuat / menambah item, di mana produk sudah dimuat:
|
|||
|
|
|
|||
|
|
| `products.sell_by` | Aturan |
|
|||
|
|
|---|---|
|
|||
|
|
| `unit` | `weight` harus kosong. Bila dikirim → tolak. |
|
|||
|
|
| `weight` | `weight` wajib ada dan `> 0`. `quantity` dipaksa `1`. |
|
|||
|
|
|
|||
|
|
`unit_id` di `order_items` diisi dari `products.unit_id` saat baris dibuat (P1) —
|
|||
|
|
bukan dibaca lewat join saat ditampilkan.
|
|||
|
|
|
|||
|
|
Berat dibulatkan ke 3 desimal saat masuk, agar nilai tersimpan selalu sama dengan
|
|||
|
|
nilai yang divalidasi.
|
|||
|
|
|
|||
|
|
### 7.2 Tampilan
|
|||
|
|
|
|||
|
|
`templates/daily_transaction.html:539` mencetak `{{$item.Quantity}}`. Untuk baris
|
|||
|
|
berbobot ini akan menampilkan `1`, bukan `4,2 ons`. Perlu bercabang pada `weight`.
|
|||
|
|
|
|||
|
|
Response API menambah `weight` dan `unit` pada item, agar frontend dan struk dapat
|
|||
|
|
menampilkan `4,2 ons × Rp 4.500` alih-alih `1 × Rp 4.500`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. Report
|
|||
|
|
|
|||
|
|
**Tidak ada perubahan yang wajib.** Karena `quantity` tetap integer dan tetap berarti
|
|||
|
|
"berapa banyak barang":
|
|||
|
|
|
|||
|
|
- `SUM(oi.quantity)` sebagai `total_items` tetap bermakna dan tetap konsisten lintas
|
|||
|
|
produk — 2 ikan tetap dihitung 2, bukan 9,8 ons.
|
|||
|
|
- `QuantitySold` tetap `int64`. Tidak ada pemotongan pecahan.
|
|||
|
|
- `average_price = SUM(total_price) / SUM(quantity)` menjadi "rata-rata harga per
|
|||
|
|
ekor", yang tetap merupakan angka bermakna.
|
|||
|
|
|
|||
|
|
**Tambahan opsional** — melaporkan berat terjual, hanya pada laporan **per produk**
|
|||
|
|
(P4):
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
COALESCE(SUM(oi.weight), 0) AS weight_sold
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Tidak boleh dipakai pada agregat lintas produk, karena akan menjumlahkan satuan yang
|
|||
|
|
berbeda.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. Di Luar Scope
|
|||
|
|
|
|||
|
|
**Pengurangan stok otomatis.** `adjustInventoryWithTransaction`
|
|||
|
|
(`order_processor.go:1177`) dan `adjustIngredientInventoryWithTransaction`
|
|||
|
|
(`order_processor.go:920`) terdefinisi tetapi **tidak pernah dipanggil dari mana pun** —
|
|||
|
|
sudah diverifikasi se-repo. Endpoint CRUD inventory berfungsi; pengurangan stok saat
|
|||
|
|
penjualan tidak tersambung.
|
|||
|
|
|
|||
|
|
Konsekuensi untuk RFC ini: `inventory.quantity` yang masih `int` tidak menghalangi
|
|||
|
|
apa pun.
|
|||
|
|
|
|||
|
|
Catatan untuk nanti bila jalur stok disambungkan:
|
|||
|
|
|
|||
|
|
- Stok produk timbangan harus berkurang sebesar `weight`, bukan `quantity` — kalau
|
|||
|
|
tidak, menjual ikan 4,2 ons hanya mengurangi stok sebanyak 1.
|
|||
|
|
- `inventory.quantity` perlu menjadi `DECIMAL(12,3)` lebih dulu. Biayanya hampir nol
|
|||
|
|
sekarang (3 call site, tanpa data historis); jauh lebih mahal setelah berjalan.
|
|||
|
|
- `order_processor.go:946` berisi `deltaInt := int(delta)` yang memotong pecahan.
|
|||
|
|
Kode ini mati, jadi bukan kebocoran aktif — tetapi bila disambungkan tanpa
|
|||
|
|
diperbaiki, konsumsi bahan di bawah 1 unit akan hilang diam-diam.
|
|||
|
|
|
|||
|
|
Kedua fungsi mati itu sebaiknya **dihapus atau disambungkan**, jangan dibiarkan
|
|||
|
|
menggantung — komentar di dalamnya ditulis seolah-olah aktif.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. Temuan Sampingan: `unit_price` pada request diabaikan
|
|||
|
|
|
|||
|
|
`CreateOrderItemRequest.UnitPrice` (`contract/order_contract.go:46`) berkomentar
|
|||
|
|
*"Optional, will use database price if not provided"*. Kenyataannya field ini **tidak
|
|||
|
|
pernah dipakai** — satu-satunya yang menyentuhnya adalah validasi `< 0` di
|
|||
|
|
`service/order_service.go:431` dan `:474`. Processor selalu membaca harga dari
|
|||
|
|
`products` / `product_outlet_prices`.
|
|||
|
|
|
|||
|
|
Perilaku sekarang sudah benar dan sesuai P3. Yang salah hanya komentarnya, yang
|
|||
|
|
menyiratkan klien bisa mengirim harga. Sebaiknya field itu **dihapus** dari contract,
|
|||
|
|
atau komentarnya dikoreksi menjadi keterangan bahwa harga selalu diambil dari database.
|
|||
|
|
|
|||
|
|
Dibiarkan seperti sekarang, ini mengundang frontend mengirim harga dan menyangka
|
|||
|
|
berhasil, padahal diabaikan diam-diam.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. Urutan Implementasi
|
|||
|
|
|
|||
|
|
1. **Migrasi skema** (§4). Aman: semua kolom nullable atau ber-default, data lama
|
|||
|
|
tidak tersentuh.
|
|||
|
|
2. **`BillableQuantity()` + `CalculateTotalPrice()` / `CalculateTotalCost()`** (§5).
|
|||
|
|
3. **Ganti 8 titik perkalian harga** (§5) lalu jalankan dua `grep` verifikasi.
|
|||
|
|
4. **Field kontrak**: `weight` pada request order & self-order, `weight` + `unit` pada
|
|||
|
|
response.
|
|||
|
|
5. **Validasi `sell_by`** (§7.1).
|
|||
|
|
6. **Template & tampilan struk** (§7.2).
|
|||
|
|
7. *(Opsional)* `weight_sold` pada laporan per produk (§8).
|
|||
|
|
|
|||
|
|
Langkah 1-4 membuat produk timbangan dapat dijual dengan harga yang benar. Langkah 5
|
|||
|
|
mencegah data tidak konsisten masuk. Langkah 6 membuat struk terbaca benar.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 12. Risiko
|
|||
|
|
|
|||
|
|
| Risiko | Dampak | Mitigasi |
|
|||
|
|
|---|---|---|
|
|||
|
|
| Satu titik perkalian harga terlewat | Ikan 4,2 ons ditagih seharga 1 ons — salah uang, tanpa error | Dua `grep` verifikasi di §5; uji satu order timbangan lewat setiap jalur (create, tambah item, void, refund, split bill) |
|
|||
|
|
| `weight` dikirim untuk produk `unit` | Harga baris salah total | Validasi §7.1 + constraint DB |
|
|||
|
|
| `quantity > 1` pada baris berbobot | `BillableQuantity()` ambigu | Dicegah `chk_order_items_weight_single_line` di level DB |
|
|||
|
|
| Klien lama tidak mengirim `weight` | Produk timbangan ditagih 1 satuan | Validasi §7.1 menolak, bukan mendiamkan |
|
|||
|
|
| Struk menampilkan `1` alih-alih `4,2 ons` | Pelanggan bingung, kasir kehilangan kepercayaan | §7.2 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 13. Pertanyaan Terbuka
|
|||
|
|
|
|||
|
|
1. **Pembulatan uang — diputuskan sementara, perlu konfirmasi.** `RoundMoney`
|
|||
|
|
membulatkan ke **2 desimal**, mengikuti presisi kolom `decimal(10,2)` yang sudah
|
|||
|
|
dipakai semua nilai uang. Jadi `4,237 ons × Rp 4.500` tersimpan `Rp 19.066,50`.
|
|||
|
|
|
|||
|
|
Ini pilihan paling tidak mengejutkan dan konsisten dengan data lama, tetapi **bukan**
|
|||
|
|
pembulatan ke rupiah utuh. Bila kasir harus menerima uang dalam rupiah penuh (atau
|
|||
|
|
kelipatan Rp 100/500), ubah `RoundMoney` di `entities/order_item.go` — satu tempat,
|
|||
|
|
dan lakukan **sebelum** ada transaksi timbangan, karena setelahnya data lama dan
|
|||
|
|
baru akan mengikuti aturan berbeda.
|
|||
|
|
2. **Presisi input berat.** Apakah `4,237 ons` (resolusi 0,1 gram) valid, atau input
|
|||
|
|
harus dibatasi ke kelipatan tertentu sesuai resolusi timbangan? Bila perlu dibatasi,
|
|||
|
|
tambahkan `products.min_weight_increment`.
|
|||
|
|
3. **Sumber angka timbangan** — kasir mengetik manual atau timbangan tersambung? Bila
|
|||
|
|
tersambung, ada urusan tara dan pembacaan stabil yang berada di luar RFC ini.
|