# BARANG — Product/Item Model

**Path:** `common/models/Barang.php`
**Table:** `barang`
**Engine:** InnoDB (MySQL / MariaDB)
**Framework:** Yii2 ActiveRecord (`common\base\MysqlActiveRecord`)

---

## 1. Overview & Business Rules

`Barang` adalah **core data model** yang merepresentasikan setiap produk, bahan baku, semi-finished good, maupun non-F&B item di dalam ekosistem POS. Seluruh alur transaksi — mulai dari pembelian ke supplier, proses produksi (WIP), hingga penjualan ke end-customer — berputar di sekitar model ini.

### 1.1 Klasifikasi Jenis Produk (`jenis`)

| Nilai (`jenis`)  | Konstanta               | Kode Prefix | Deskripsi                                                                   |
|-----------------|-------------------------|-------------|-----------------------------------------------------------------------------|
| `"Finish"`      | `Constanta::JENIS_FG`   | `MN`        | Finish Good — menu/produk yang dijual langsung ke customer                  |
| `"Raw"`         | `Constanta::JENIS_RAW`  | `RM`        | Raw Material — bahan baku yang dikonsumsi saat produksi atau penjualan      |
| `"Wip"`         | `Constanta::JENIS_WIP`  | `RC`        | Work In Progress — produk setengah jadi, memiliki recipe sendiri            |
| `"NonFnb"`      | `Constanta::JENIS_NONFNB`| `NF`       | Non Food & Beverage — item operasional (ATK, peralatan, dsb.)               |

### 1.2 Sub-Type untuk WIP & FG (`type`)

| Nilai (`type`)      | Konstanta                       | Deskripsi                                                |
|--------------------|--------------------------------|----------------------------------------------------------|
| `"finish goods"`   | `Constanta::TYPE_WIP_MENU`     | Finish Good dengan recipe bahan baku                     |
| `"wip"`            | `Constanta::TYPE_WIP_WIP`      | WIP yang diproses secara batch sebelum penjualan         |
| `"addon"`          | `Constanta::TYPE_WIP_ADDON`    | Add-on recipe (topping, saus, dsb.)                      |
| `"simulation"`     | `Constanta::TYPE_WIP_SIMULATION`| Draft simulasi, tidak diproses ke stok nyata            |

### 1.3 Aturan Bisnis Kritikal

1. **Field `nama` wajib diisi** — satu-satunya hard required constraint di level model.
2. **Field `code` bersifat UNIQUE** — digunakan sebagai identifier eksternal (barcode, QR, integrasi marketplace).
3. **Stok tidak disimpan di tabel `barang`** — stok aktual hidup di tabel `stok` (ledger mutasi per-subwarehouse) dan `barang_stok` (snapshot per-cabang). Kolom `stok_awal`/`stok_ahir` di `barang_stok` adalah denormalisasi untuk performa query kasir.
4. **Barang dengan `stok_ignore = 1`** dikecualikan dari kalkulasi stok FG (`getFgStock()`). Digunakan untuk bahan yang tidak perlu dilacak habisnya (misalnya bumbu dasar).
5. **Soft-delete** melalui kolom `deleted_at`. Record tidak dihapus secara fisik.
6. **Cache invalidasi otomatis** — setelah setiap `save()` atau `delete()`, `TagDependency::invalidate()` dipanggil dengan key `barang_{id}` untuk memastikan konsistensi cache layer.
7. **Barang `is_draft = 1`** hanya digunakan untuk simulasi biaya; tidak akan muncul di kasir atau laporan produksi.
8. **Barang `is_consignment = 1`** dikecualikan dari kalkulasi stok alert.

---

## 2. Skema Database

### 2.1 Tabel `barang` (Core Table)

| Nama Kolom        | Tipe Data                          | Constraints                         | Deskripsi                                                             |
|------------------|------------------------------------|-------------------------------------|-----------------------------------------------------------------------|
| `id`             | `BIGINT UNSIGNED`                  | `PK`, `AUTO_INCREMENT`              | Primary key                                                           |
| `code`           | `VARCHAR(20)`                      | `UNIQUE`, `NULLABLE`                | Kode produk unik (barcode / SKU)                                      |
| `avatar`         | `VARCHAR(255)`                     | `NULLABLE`                          | Path relatif file gambar produk                                       |
| `nama`           | `VARCHAR(100)`                     | `NOT NULL`                          | Nama produk                                                           |
| `harga_beli`     | `INT(10) UNSIGNED`                 | `NULLABLE`                          | Harga beli default (override oleh data pembelian aktual)              |
| `satuan`         | `INT UNSIGNED`                     | `FK → satuan.id`, `NULLABLE`        | Unit dasar / base unit (misal: gram, pcs)                             |
| `satuan_pkg`     | `INT UNSIGNED`                     | `FK → satuan.id`, `NULLABLE`        | Unit packaging default untuk pembelian/penjualan                      |
| `konversi_pkg`   | `DOUBLE`                           | `NULLABLE`                          | Faktor konversi `satuan_pkg` ke `satuan` (unit dasar)                 |
| `j_satuan_pkg`   | `TEXT` (JSON)                      | `NULLABLE`                          | Array JSON multi-unit packaging tambahan                              |
| `jenis`          | `VARCHAR(45)`                      | `NULLABLE`                          | Klasifikasi utama: `Finish`, `Raw`, `Wip`, `NonFnb`                   |
| `type`           | `VARCHAR(20)`                      | `NULLABLE`                          | Sub-type untuk WIP/FG: `finish goods`, `wip`, `addon`, `simulation`   |
| `katagori_id`    | `INT(10) UNSIGNED`                 | `FK → katagori.id`, `NULLABLE`      | Foreign key ke kategori menu                                          |
| `produsen_id`    | `BIGINT UNSIGNED`                  | `FK → produsen.id`, `NULLABLE`      | Supplier/produsen default untuk raw material ini                      |
| `barang_group_id`| `BIGINT UNSIGNED`                  | `FK → barang_group.id`, `NULLABLE`  | Pengelompokan produk untuk laporan P&L                                |
| `parent_id`      | `INT UNSIGNED`                     | `NULLABLE`                          | Self-referencing parent (untuk produk turunan)                        |
| `linked_fg`      | `BIGINT UNSIGNED`                  | `FK → barang.id`, `NULLABLE`        | WIP yang di-assign ke menu recipe (FG ↔ WIP linkage)                 |
| `is_bundled`     | `TINYINT(1) UNSIGNED`              | `DEFAULT 0`                         | Flag produk bundle (paket)                                            |
| `is_draft`       | `TINYINT(1)`                       | `DEFAULT 0`                         | Flag simulasi; tidak masuk stok dan kasir                             |
| `is_consignment` | `TINYINT(1)`                       | `DEFAULT 0`                         | Flag barang konsinyasi                                                |
| `as_addon`       | `TINYINT(1)`                       | `DEFAULT 0`                         | Flag item dapat dijadikan add-on                                      |
| `cook_type`      | `TINYINT(1)`                       | `DEFAULT 0`                         | `0` = WIP step (proses produksi), `1` = cook on sales (langsung jual) |
| `stok_ignore`    | `TINYINT(1)`                       | `DEFAULT 0`                         | Jika `1`, stok item ini diabaikan di kalkulasi FG stock               |
| `hasil_jadi`     | `DOUBLE`                           | `NULLABLE`                          | Yield produksi WIP dalam unit dasar                                   |
| `hasil_jadi_qty` | `INT(4) UNSIGNED`                  | `NULLABLE`                          | Qty batch produksi untuk satu siklus recipe                           |
| `outlets`        | `TEXT`                             | `NULLABLE`                          | JSON — daftar outlet yang menjual produk ini                          |
| `keterangan`     | `VARCHAR(250)`                     | `NULLABLE`                          | Catatan/deskripsi produk                                              |
| `urutan`         | `INT(2) UNSIGNED`                  | `DEFAULT 0`                         | Urutan tampil di menu kasir                                           |
| `createAt`       | `DATETIME`                         | `NULLABLE`                          | Timestamp pembuatan record (auto-set oleh TimestampBehavior)          |
| `updateAt`       | `DATETIME`                         | `NULLABLE`                          | Timestamp update terakhir (auto-set oleh TimestampBehavior)           |
| `deleted_at`     | `DATETIME`                         | `NULLABLE`                          | Soft-delete timestamp; `NULL` = aktif                                 |
| `created_by`     | `INT UNSIGNED`                     | `FK → user.id`, `NULLABLE`          | User yang membuat record (BlameableBehavior)                          |
| `updated_by`     | `INT UNSIGNED`                     | `FK → user.id`, `NULLABLE`          | User yang terakhir mengupdate (BlameableBehavior)                     |

> **Catatan:** Kolom `j_recipe` (TEXT) pernah ada di migrasi awal namun fungsinya telah dipindahkan ke tabel `barang_recipe`. Kolom ini mungkin masih ada di database tetapi tidak lagi digunakan di application layer.

### 2.2 Tabel `barang_stok` (Per-Cabang Config & Snapshot)

Tabel pivot antara `barang` dan `cabang`. Menyimpan konfigurasi harga, visibilitas, dan snapshot stok per outlet.

| Nama Kolom              | Tipe Data        | Deskripsi                                              |
|------------------------|-----------------|--------------------------------------------------------|
| `barang_id`            | `BIGINT`        | FK → `barang.id`                                       |
| `cabang_id`            | `INT`           | FK → `cabang.id`                                       |
| `harga_s`              | `DOUBLE`        | Harga jual utama (standard price)                      |
| `harga_s_active`       | `INT`           | Flag aktif harga standard                              |
| `harga_jual_toko`      | `INT`           | Harga jual di toko                                     |
| `harga_jual_mkt`       | `INT`           | Harga jual marketplace/online                          |
| `harga_jual_grosir_toko`| `INT`          | Harga grosir toko                                      |
| `harga_jual_grosir_mkt`| `INT`           | Harga grosir marketplace                               |
| `min_grosir`           | `INT`           | Minimum qty untuk harga grosir berlaku                 |
| `harga_coret`          | `INT`           | Harga coret (strike-through price untuk promo)         |
| `pajak`                | `DECIMAL`       | Persentase PPN yang dibebankan                         |
| `service_charge`       | `DECIMAL`       | Persentase service charge                              |
| `stok_awal`            | `DECIMAL`       | Stok awal periode (snapshot)                           |
| `stok_ahir`            | `DECIMAL`       | Stok akhir terkini (snapshot — diupdate via queue)     |
| `stok_alert`           | `DECIMAL`       | Threshold minimum stok untuk trigger alert             |
| `last_harga_beli`      | `DOUBLE`        | Harga beli terakhir (dipakai untuk cost FG/WIP)        |
| `show_cashier`         | `INT`           | Flag tampil di kasir                                   |
| `show_kiosk`           | `INT`           | Flag tampil di kiosk                                   |
| `show_mobile`          | `INT`           | Flag tampil di aplikasi mobile                         |
| `hide`                 | `VARCHAR`       | `Y`/`N` — sembunyikan dari menu                        |
| `delete`               | `VARCHAR`       | `Y`/`N` — soft-delete per-cabang                       |
| `j_discount`           | `TEXT` (JSON)   | Konfigurasi diskon bertingkat                          |
| `j_payment`            | `TEXT` (JSON)   | Konfigurasi metode pembayaran yang diterima            |
| `j_addon`              | `TEXT` (JSON)   | Daftar add-on yang tersedia                            |
| `j_variant`            | `TEXT` (JSON)   | Daftar varian (ukuran, rasa, dsb.)                     |
| `j_price`              | `TEXT` (JSON)   | Harga per ukuran (S/M/L)                               |
| `j_bundled_items`      | `TEXT` (JSON)   | Preference groups untuk produk bundle                  |

### 2.3 Tabel `barang_recipe` (Bill of Materials)

| Nama Kolom        | Tipe Data        | Constraints              | Deskripsi                                                        |
|------------------|-----------------|--------------------------|------------------------------------------------------------------|
| `id`             | `INT`           | `PK`                     | Primary key                                                      |
| `barang_id`      | `INT`           | `FK → barang.id`, `NOT NULL` | Produk jadi / parent (FG atau WIP)                           |
| `item_id`        | `INT`           | `FK → barang.id`, `NOT NULL` | Bahan baku / ingredient                                      |
| `type`           | `VARCHAR(50)`   | `NULLABLE`               | Jenis ingredient (`raw`, `finish goods`, dsb.)                   |
| `amount`         | `DOUBLE`        | `NULLABLE`               | Jumlah bahan **per unit produk** (dibagi `qty`)                  |
| `qty`            | `DOUBLE`        | `NULLABLE`               | Batch qty saat recipe disimpan                                   |
| `conversion`     | `DOUBLE`        | `NULLABLE`               | Faktor konversi unit bahan ke unit dasar                         |
| `unitCost`       | `DOUBLE`        | `NULLABLE`               | Harga beli per unit ingredient saat recipe dibuat                |
| `itemCost`       | `DOUBLE`        | `NULLABLE`               | Total biaya ingredient per unit produk                           |
| `unit`           | `VARCHAR(20)`   | `NULLABLE`               | Satuan tampil ingredient (nama string)                           |
| `recommendedPrice`| `INT`          | `DEFAULT 0`              | Rekomendasi harga jual dari kalkulasi recipe                     |
| `active`         | `INT`           | `DEFAULT 1`              | Flag aktif                                                       |

### 2.4 Tabel `stok` (Stock Ledger)

Setiap baris adalah **satu mutasi stok** (insert-only ledger). Stok aktual dihitung dari `latest_qty` pada record terakhir per `barang_id + subwarehouse_id`.

| Nama Kolom        | Tipe Data        | Constraints              | Deskripsi                                                        |
|------------------|-----------------|--------------------------|------------------------------------------------------------------|
| `id`             | `BIGINT`        | `PK`                     | Primary key                                                      |
| `barang_id`      | `BIGINT`        | `FK → barang.id`         | Produk yang dimutasi                                             |
| `cabang_id`      | `INT`           | `FK → cabang.id`         | Cabang/outlet                                                    |
| `subwarehouse_id`| `INT`           | `FK → subwarehouse.id`   | Sub-gudang (department, dapur, bar, dsb.)                        |
| `qty`            | `DOUBLE`        | —                        | Delta mutasi: **positif** = masuk, **negatif** = keluar          |
| `latest_qty`     | `DOUBLE`        | —                        | Running balance stok setelah mutasi ini                          |
| `type`           | `VARCHAR(10)`   | —                        | Jenis mutasi (lihat konstanta `Stok::TYPE_*`)                    |
| `id_ref`         | `INT`           | —                        | ID referensi dokumen (penjualan_id, pembelian_id, dsb.)          |
| `desc`           | `VARCHAR(1000)` | —                        | Keterangan naratif mutasi                                        |
| `buy_price_id`   | `INT`           | `FK → buy_price.id`      | Referensi harga beli untuk COGS tracking                         |
| `used_at`        | `DATETIME(6)`   | —                        | Timestamp efektif mutasi (microsecond precision)                 |
| `created_at`     | `DATETIME`      | —                        | Timestamp insert record                                          |
| `satuan_id`      | `INT`           | `FK → satuan.id`         | ID unit dasar saat mutasi                                        |
| `satuan_name`    | `VARCHAR(50)`   | —                        | Nama unit (denormalisasi untuk performa)                         |
| `is_consignment` | `TINYINT`       | —                        | Flag mutasi barang konsinyasi                                    |

---

## 3. Relasi Antar Tabel (Entity Relationships)

```
barang
│
├── BelongsTo ──────► katagori          (barang.katagori_id → katagori.id)
│                     Satu barang dimiliki oleh satu kategori menu.
│
├── BelongsTo ──────► produsen          (barang.produsen_id → produsen.id)
│                     Supplier/produsen default untuk barang ini.
│
├── BelongsTo ──────► satuan [base]     (barang.satuan → satuan.id)
│                     Unit terkecil / unit dasar (gram, pcs, ml, dsb.)
│
├── BelongsTo ──────► satuan [pkg]      (barang.satuan_pkg → satuan.id)
│                     Unit pembelian/penjualan default.
│
├── BelongsTo ──────► barang_group      (barang.barang_group_id → barang_group.id)
│                     Pengelompokan produk untuk laporan P&L.
│
├── BelongsTo ──────► barang [linked]   (barang.linked_fg → barang.id) [self-referential]
│                     WIP yang dikaitkan ke Finish Good ini.
│
├── HasMany ─────────► barang_stok      (barang.id → barang_stok.barang_id)
│                     Konfigurasi harga & stok snapshot per cabang.
│
├── HasMany ─────────► stok             (barang.id → stok.barang_id)
│                     Ledger mutasi stok historis (insert-only).
│
├── HasMany ─────────► barang_recipe [as parent]  (barang.id → barang_recipe.barang_id)
│                     Recipe (BOM) dimana barang ini adalah produk jadi.
│
├── HasMany ─────────► barang_recipe [as ingredient]  (barang.id → barang_recipe.item_id)
│                     Recipe dimana barang ini adalah bahan baku.
│
├── HasMany ─────────► barang_item [as child]     (barang.id → barang_item.barang_id)
│                     Relasi bundle: barang ini adalah komponen dari produk bundle lain.
│
├── HasMany ─────────► barang_item [as parent]    (barang.id → barang_item.barang_id_parent)
│                     Relasi bundle: barang ini adalah induk/paket.
│
├── HasMany ─────────► penjualan_item   (barang.id → penjualan_item.barang_id)
│   └── HasOne ──────► penjualan        (penjualan_item.penjualan_id → penjualan.id)
│                     Riwayat penjualan ke customer (melalui pivot penjualan_item).
│
└── HasMany ─────────► pembelian_item   (barang.id → pembelian_item.barang_id)
    └── HasOne ──────► pembelian        (pembelian_item.pembelian_id → pembelian.id)
                      Riwayat pembelian dari supplier (melalui pivot pembelian_item).
```

### 3.1 Multi-Unit (UOM) Architecture

Satu produk dapat memiliki **lebih dari satu unit** yang tersimpan di dua kolom berbeda:

| Storage            | Kolom              | Kapasitas     | Keterangan                                  |
|-------------------|--------------------|---------------|---------------------------------------------|
| **Unit Dasar**    | `satuan`           | 1 unit        | Base unit; faktor konversi = 1              |
| **Unit Pkg Utama**| `satuan_pkg` + `konversi_pkg` | 1 unit | Default packaging unit                  |
| **Unit Pkg Ekstra**| `j_satuan_pkg` (JSON) | N unit   | Array unit tambahan dengan konversi masing-masing |

Fungsi `getAllUnitThisBarang($includeUnitStd)` menggabungkan ketiga storage ini menjadi satu array yang konsisten.

**Pengecualian — Jenis WIP:** Unit yang valid hanya "Portion" dengan konversi diambil dari `hasil_jadi`.

---

## 4. Mutasi & Manajemen Stok

### 4.1 Arsitektur Stok Ledger

Sistem stok menggunakan **append-only ledger** di tabel `stok`. Setiap event yang mempengaruhi inventori menghasilkan satu baris baru dengan:
- `qty` = delta (positif untuk masuk, negatif untuk keluar)
- `latest_qty` = running balance terkini

**Single entry point:** Semua mutasi harus melalui `Stok::finalSave()`. Method ini:
1. Menormalisasi timestamp `used_at` ke presisi microsecond.
2. Mengambil **MySQL Advisory Lock** per `barang_id + subwarehouse_id` untuk mencegah race condition.
3. Membaca `latest_qty` dari record sebelumnya (berdasarkan `used_at`).
4. Menyimpan record baru.
5. Menjalankan `recalculateLatestStock()` untuk memperbaiki semua record sesudah `used_at` jika ada mutasi backdating.
6. Melepas lock.

### 4.2 Jenis Mutasi Stok (`Stok::TYPE_*`)

| Konstanta               | Nilai String   | Arah  | Trigger Event                                          |
|------------------------|----------------|-------|--------------------------------------------------------|
| `TYPE_SALES`           | `"sales"`      | `-`   | Penjualan dikonfirmasi/dibayar di kasir                |
| `TYPE_PURCHASE`        | `"purchase"`   | `+`   | Pembelian langsung (petty cash / SPP)                  |
| `TYPE_GOODS_RECEIVE`   | `"gr"`         | `+`   | Goods Receipt dari Purchase Order diterima             |
| `TYPE_OPENING`         | `"opening"`    | `+`   | Stok awal periode / pembukaan cabang baru              |
| `TYPE_BEGINING`        | `"begining"`   | `+`   | Stok awal historis (data migration)                    |
| `TYPE_ADJUSTMENT`      | `"adjustment"` | `±`   | Penyesuaian manual oleh admin (opname)                 |
| `TYPE_TRANSFER_IN`     | `"tfIn"`       | `+`   | Transfer stok masuk dari gudang/cabang lain            |
| `TYPE_TRANSFER_OUT`    | `"tfOut"`      | `-`   | Transfer stok keluar ke gudang/cabang lain             |
| `TYPE_WIP`             | `"wip"`        | `-`   | Konsumsi bahan baku saat proses produksi WIP           |
| `TYPE_WIP_PART`        | `"wip_part"`   | `+`   | Hasil produksi WIP masuk sebagai stok                  |
| `TYPE_PETTY_CASH`      | `"pty"`        | `+/-` | Pembelian via petty cash                               |

### 4.3 Pengurangan Stok saat Penjualan

Alur pengurangan stok ketika transaksi penjualan diselesaikan:

```
Kasir konfirmasi pembayaran
        │
        ▼
PenjualanController::actionConfirm()
        │
        ├─── Untuk setiap penjualan_item:
        │        │
        │        ├─ [jenis == "Finish" / FG dengan recipe]
        │        │       └─► Iterasi barang_recipe → kurangi stok tiap ingredient (Raw/WIP)
        │        │               Stok::finalSave() dengan:
        │        │                 qty    = -(recipe.amount * penjualan_item.qty)
        │        │                 type   = Stok::TYPE_SALES
        │        │                 id_ref = penjualan_id
        │        │
        │        ├─ [jenis == "Raw" / "NonFnb" — tanpa recipe]
        │        │       └─► Kurangi stok langsung
        │        │               Stok::finalSave() dengan:
        │        │                 qty    = -(penjualan_item.qty)
        │        │                 type   = Stok::TYPE_SALES
        │        │                 id_ref = penjualan_id
        │        │
        │        └─ [stok_ignore == 1]
        │                └─► Skip — tidak ada mutasi stok
        │
        └─── Update snapshot barang_stok.stok_ahir (via queue atau langsung)
```

**Kalkulasi Stok FG dari Recipe** (`Barang::getFgStock($cbg_id)`):

Untuk produk FG yang memiliki recipe, stok "virtual" yang tersedia dihitung sebagai:

```
min( floor(stok_ingredient_i / recipe_amount_i) )  untuk semua i dalam recipe
```

Jika ingredient memiliki `stok_ignore = 1`, ingredient tersebut di-skip dari kalkulasi.

### 4.4 Penambahan Stok (Purchase Order / Goods Receipt)

Alur penambahan stok ketika Purchase Order (PO) diterima sebagai Goods Receipt (GR):

```
Admin konfirmasi Penerimaan Barang (GR)
        │
        ▼
PenerimaanBarangController::actionConfirm()
        │
        ├─── Validasi: pastikan GR belum pernah diproses (cek stok.id_ref + type == "gr")
        │
        ├─── Untuk setiap penerimaan_barang_item:
        │        └─► Stok::finalSave() dengan:
        │               barang_id       = item.barang_id
        │               subwarehouse_id = item.subwarehouse_id
        │               qty             = +item.receive_qty (dalam unit dasar, sudah dikonversi)
        │               type            = Stok::TYPE_GOODS_RECEIVE ("gr")
        │               id_ref          = penerimaan_barang_id
        │               buy_price_id    = referensi BuyPrice untuk COGS tracking
        │               desc            = "GR #{gr_number}"
        │
        └─── Update: pembelian.gr_status = STATUS_RECEIVED
                     barang_stok.last_harga_beli = harga beli dari PO item
```

**Pembatalan GR (Reversal):**
```sql
DELETE FROM stok
WHERE id_ref = :pembelian_id
  AND type   = 'gr'
  AND cabang_id = :cabang_id;
```
Setelah delete, `recalculateLatestStock()` dipanggil untuk memperbaiki running balance semua record sesudahnya.

### 4.5 Penambahan Stok via Retur Penjualan

Jika penjualan di-retur, stok dikembalikan dengan mutasi positif bertipe `TYPE_ADJUSTMENT`, mengacu `id_ref` ke `penjualan_id` asli.

---

## 5. JSON Payload / Serializer Standard

### 5.1 Response API — Detail Produk dengan Relasi

Berikut adalah contoh response JSON untuk endpoint `GET /v1/menu/{id}` atau setara, yang mengembalikan data detail produk beserta relasi yang di-embed:

```json
{
  "id": 42,
  "code": "MN-042",
  "nama": "Cappuccino Special",
  "jenis": "Finish",
  "type": "finish goods",
  "harga_beli": 8500,
  "stok_ignore": 0,
  "is_bundled": 0,
  "is_draft": 0,
  "is_consignment": 0,
  "cook_type": 1,
  "hasil_jadi": null,
  "hasil_jadi_qty": null,
  "linked_fg": null,
  "avatar": "/picture/product/cappuccino-special.jpg",
  "keterangan": "Espresso dengan steamed milk foam, ukuran 250ml",
  "urutan": 3,
  "createAt": "2024-01-15 08:30:00",
  "updateAt": "2025-03-10 14:22:00",
  "deleted_at": null,

  "katagori": {
    "id": 7,
    "nama": "Kopi",
    "top_id": 2,
    "warna": "#8B4513",
    "jenis": "fnb",
    "deleted_at": null
  },

  "produsen": {
    "id": 3,
    "nama": "PT Biji Kopi Nusantara",
    "kota": "Jakarta",
    "kontak": "021-555-1234"
  },

  "satuanx": {
    "id": 14,
    "name": "Gram",
    "format": "g",
    "type": "weight"
  },

  "satuanPkg": {
    "id": 22,
    "name": "Cup",
    "format": "cup",
    "type": "pkg"
  },

  "barang_group": {
    "id": 5,
    "name": "Beverages Hot"
  },

  "all_units": [
    {
      "id": 14,
      "unit_pkg": "Gram",
      "conversion": 1,
      "unit_std": "Gram",
      "format": "g",
      "active": 1
    },
    {
      "id": 22,
      "unit_pkg": "Cup",
      "conversion": 250,
      "unit_std": "Gram",
      "format": "cup",
      "active": 1
    }
  ],

  "recipes": [
    {
      "id": 101,
      "item_id": 15,
      "name": "Espresso Shot",
      "amount": 18.0,
      "unit": "g",
      "conversion": 1,
      "unitCost": 450.0,
      "itemCost": 8100.0,
      "active": 1
    },
    {
      "id": 102,
      "item_id": 28,
      "name": "Fresh Milk",
      "amount": 200.0,
      "unit": "ml",
      "conversion": 1,
      "unitCost": 20.0,
      "itemCost": 4000.0,
      "active": 1
    }
  ],

  "barang_stok": [
    {
      "cabang_id": 1,
      "harga_s": 28000,
      "harga_jual_toko": 28000,
      "harga_jual_mkt": 30000,
      "pajak": 10,
      "service_charge": 5,
      "stok_awal": 100,
      "stok_ahir": 47,
      "stok_alert": 10,
      "last_harga_beli": 12500,
      "show_cashier": 1,
      "hide": "N",
      "delete": "N"
    }
  ]
}
```

### 5.2 Struktur JSON `j_satuan_pkg` (Multi-Unit Field)

```json
[
  {
    "id": 22,
    "unit_pkg": "Cup",
    "conversion": 250,
    "unit_std": "Gram",
    "active": 1
  },
  {
    "id": 33,
    "unit_pkg": "Liter",
    "conversion": 1000,
    "unit_std": "Gram",
    "active": 1
  }
]
```

### 5.3 Struktur JSON `j_bundled_items` (Bundle Preference Groups)

```json
[
  {
    "prefId": "1",
    "name": "Pilih Minuman",
    "mandatory": 1,
    "min_select": 1,
    "max_select": 1,
    "stores": "",
    "items": [
      {
        "id": 42,
        "idx_pref_local": "042",
        "name": "Cappuccino Special",
        "qty": 1,
        "auto_select": 0,
        "mandatory": 1,
        "amount": 0
      },
      {
        "id": 43,
        "idx_pref_local": "043",
        "name": "Latte",
        "qty": 1,
        "auto_select": 0,
        "mandatory": 1,
        "amount": 5000
      }
    ]
  }
]
```

### 5.4 Response `showQtyOnSelectedUnit()` — Konversi Unit

Digunakan oleh kasir dan laporan untuk menampilkan qty dalam unit yang dipilih user:

```json
{
  "id": 22,
  "qty": 750,
  "convertedQty": 3,
  "baseQty": 187500,
  "unit_pkg": "Cup",
  "format": "cup",
  "conversion": 250,
  "unit_std": "Gram"
}
```

> `convertedQty = qty / conversion` — qty dalam unit tampil  
> `baseQty = qty * conversion` — qty dalam unit dasar

---

## 6. Caching Strategy

Model ini menggunakan dua layer cache:

| Cache Key Pattern    | TTL    | Invalidasi                                    | Data                                    |
|---------------------|--------|-----------------------------------------------|-----------------------------------------|
| `barang_{id}`       | 3600 s | `afterSave()` / `afterDelete()` via TagDependency | Single record by ID                 |
| `barang_all`        | 3600 s | `MAX(updateAt)` DbDependency                  | Semua FG aktif (id, code, nama, katagori_id) |
| `barang_group_{id}` | 3600 s | Manual (set saat query)                       | Data BarangGroup                        |
| `satuan_{id}`       | 3600 s | `Satuan::afterSave()` TagDependency           | Data Satuan                             |

> `getCachedMenu()` mengimplementasikan **per-ID cache** dengan fallback batch-load untuk ID yang belum di-cache, menghindari N+1 query ke database.

---

## 7. Behaviors & Hooks

| Behavior                 | Fungsi                                                              |
|--------------------------|---------------------------------------------------------------------|
| `TimestampBehavior`      | Auto-set `createAt` / `updateAt`                                    |
| `BlameableBehavior`      | Auto-set `created_by` / `updated_by` dari `Yii::$app->user->id`    |
| `ActivityLogBehavior`    | Mencatat setiap perubahan atribut ke tabel `activity_log`           |
| `TagDependency` (manual) | Invalidasi cache `barang_{id}` via `afterSave()` dan `afterDelete()` |

---

## 8. Indeks Database yang Direkomendasikan

```sql
-- Sudah ada dari migrasi:
INDEX fk_barang_katagori1_idx (katagori_id)
INDEX fk_barang_produsen_idx  (produsen_id)
UNIQUE INDEX (code)

-- Direkomendasikan untuk performa query umum:
INDEX idx_barang_jenis        (jenis)
INDEX idx_barang_deleted_at   (deleted_at)
INDEX idx_barang_group        (barang_group_id)

-- Untuk stok ledger:
INDEX idx_stok_barang_subwh_usedat (barang_id, subwarehouse_id, used_at)
INDEX idx_stok_type_idref          (type, id_ref)
```

---

## 9. File Terkait

| File                                          | Peran                                                   |
|----------------------------------------------|---------------------------------------------------------|
| `common/models/Barang.php`                   | Model utama (dokumen ini)                               |
| `common/models/BarangStok.php`               | Per-cabang config, snapshot stok, harga                 |
| `common/models/BarangRecipe.php`             | Bill of Materials / recipe ingredients                  |
| `common/models/BarangItem.php`               | Bundle component mapping                                |
| `common/models/BarangGroup.php`              | Pengelompokan produk                                    |
| `common/models/Stok.php`                     | Stock ledger + `finalSave()` + `recalculateLatestStock()` |
| `common/models/PenjualanItem.php`            | Pivot transaksi penjualan                               |
| `common/models/PembelianItem.php`            | Pivot transaksi pembelian                               |
| `common/models/Satuan.php`                   | Master unit of measure                                  |
| `common/models/Katagori.php`                 | Master kategori produk                                  |
| `common/models/Produsen.php`                 | Master supplier                                         |
| `common/base/MyStockManagement.php`          | Kalkulasi COGS dan cost recipe (recursive)              |
| `common/Constanta.php`                       | Semua konstanta `JENIS_*`, `TYPE_*`, `COOK_TYPE_*`      |
