# Aturan Modul DPB & Penentuan Harga Obat

**Tipe:** Spesifikasi bisnis + teknis (sumber kebenaran istilah & rumus)  
**Status:** Living document — selaraskan dengan `business-rules.md` dan `database.md`  
**Terakhir diselaraskan:** 23 Juli 2026  
**Acuan Excel:** [`contoh_tabel_harga.xlsx`](contoh_tabel_harga.xlsx)

> Baca dokumen ini sebelum mengubah `PembelianService`, `HargaPerhitunganService`, `HargaModalService`, `HargaJualService`, atau UI konfirmasi harga.

---

## 1. Prinsip inti

| # | Prinsip | Alasan |
|---|---------|--------|
| 1 | Stok & harga beli **bukan** di `master_obat` | Satu obat punya banyak batch/PBF/harga |
| 2 | `stok_batch_obat.harga_beli` & `harga_modal` per batch **tidak dirata-ratakan di DB** | Agregat hanya di service |
| 3 | `detail_pembelian` / `detail_penjualan` = **snapshot** | Audit; tidak diubah retroaktif |
| 4 | Harga jual aktif di `harga_jual_obat` | Satu aktif per `(obat_id, cabang_id)` |
| 5 | DPB dan penentuan harga jual = **dua langkah** | Simpan stok/tagihan dulu → review di Manajemen Harga (opsional shortcut per faktur) |
| 6 | Margin VIP &lt; 11% = **warning**, bukan hard block | Admin boleh override |
| 7 | **OTC = ecer**; **REG/VIP = kemasan utuh** | Sesuai praktik Excel apotek |

---

## 2. Kamus istilah (wajib dibedakan)

### 2.1 Level kemasan vs ecer

| Istilah | Arti operasional | Contoh |
|---------|------------------|--------|
| **Kemasan utuh** | Satu unit jual terbesar di master (`STRIP`, `BOTOL`, …) = `master_satuan_obat` + `isi_kemasan` | 1 STRIP isi 10 tablet |
| **Satuan ecer** | Isi di dalam kemasan | 1 tablet |
| **`isi_kemasan`** | Jumlah satuan ecer per 1 kemasan utuh | `10` |

### 2.2 Rantai harga dari faktur PBF

| Istilah | Label UI | Definisi | Disimpan di |
|---------|----------|----------|-------------|
| **Harga beli** | Harga beli | Harga satuan kemasan di faktur PBF (sebelum diskon) | `detail_pembelian.harga_beli`, `stok_batch_obat.harga_beli` |
| **Diskon** | Diskon (%) | Potongan persen per baris faktur | `detail_pembelian.diskon_persen` |
| **Harga beli netto** | — | Harga beli setelah diskon, **sebelum** PPN | `detail_pembelian.harga_beli_netto` |
| **Harga modal (kemasan)** | Modal kemasan | Netto × PPN 11% — modal **per kemasan utuh** | `detail_pembelian.harga_modal`, `stok_batch_obat.harga_modal` |
| **Harga markup** | Harga markup (×1,015) | Modal kemasan × 1,015 | Tidak wajib kolom DB; dihitung live |
| **Harga modal satuan / eceran** | Harga modal eceran / satuan | Markup ÷ `isi_kemasan` — dasar OTC & semua margin | `detail_pembelian.harga_modal_satuan`, `harga_jual_obat.harga_modal_satuan` |
| **Modal faktur** | Modal faktur (kemasan) | Rata-rata tertimbang `harga_modal` dari baris DPB ini (per obat) | Dihitung (`HargaModalService`) |
| **Modal agregat** | Modal agregat (kemasan) | Agregat **satu titik harga per PBF** di cabang (`rata_rata` = mean titik terbaru; `tertinggi` = max titik max-historis) | → `harga_jual_obat.harga_modal_terhitung` |

### 2.3 Harga jual

| Istilah | Label UI | Level | Acuan saat admin menentukan | Kolom DB |
|---------|----------|-------|-----------------------------|----------|
| **Harga OTC** | Harga OTC (ecer) | **Ecer** | `harga_modal_satuan` | `harga_jual_obat.harga_otc` |
| **Harga REG** | Harga REG (STRIP/…) | **Kemasan utuh** | `harga_modal` (agregat) | `harga_jual_obat.harga_reg` |
| **Harga VIP** | Harga VIP (STRIP/…) | **Kemasan utuh** | `harga_modal` (agregat) | `harga_jual_obat.harga_vip` |
| **Harga OTC satuan** | Harga satuan OTC | Ecer | = `harga_otc` (sudah ecer, **jangan dibagi** isi) | `harga_otc_satuan` |
| **Harga REG/VIP satuan** | Harga satuan REG/VIP | Ecer (turunan) | `harga_reg\|vip / isi_kemasan` | `harga_reg_satuan`, `harga_vip_satuan` |
| **Margin OTC/REG/VIP** | % OTC / % Reguler / % VIP | Persen | Semua vs `harga_modal_satuan` | `margin_otc`, `margin_reg`, `margin_vip` |

**Mengapa OTC vs REG/VIP dibedakan level?**  
Pelanggan umum (OTC) sering beli ecer. Pelanggan medis (REG/VIP) jarang beli ecer — lebih sering kemasan utuh.

### 2.4 Total tagihan DPB (berbeda dari harga modal per unit)

| Istilah | Definisi | Digunakan untuk |
|---------|----------|-----------------|
| **Subtotal baris / DPP baris** | `jumlah × harga_beli − diskon` | `detail_pembelian.subtotal` |
| **Subtotal DPP faktur** | Σ subtotal baris | Dasar PPN tagihan |
| **PPN tagihan 11%** | `subtotal_dpp × 0.11` | Bagian tagihan ke PBF |
| **Total faktur / total tagihan** | `subtotal_dpp + nilai_ppn` | `pembelian.total_faktur`, `tagihan_pbf.nilai_faktur` |

> **Jangan campur:** PPN di **harga modal per unit** (`harga_beli_netto × 1.11`) dipakai untuk penentuan harga jual.  
> PPN di **total tagihan** (`Σ subtotal × 1.11`) dipakai untuk hutang ke PBF. Keduanya memakai faktor 11%, tetapi basisnya berbeda.

---

## 3. Relasi database

```mermaid
erDiagram
    master_obat ||--o{ detail_pembelian : dibeli
    master_obat ||--o{ stok_batch_obat : distok
    master_obat ||--o{ harga_jual_obat : dijual
    master_obat }o--|| master_satuan_obat : kemasan

    master_pbf ||--o{ pembelian : memasok
    master_cabang ||--o{ pembelian : menerima
    master_cabang ||--o{ stok_batch_obat : menyimpan
    master_cabang ||--o{ harga_jual_obat : scoped

    pembelian ||--|{ detail_pembelian : berisi
    pembelian ||--o| tagihan_pbf : menghasilkan
    pembelian ||--o{ harga_jual_obat : memicu

    detail_pembelian ||--o| stok_batch_obat : menciptakan
    stok_batch_obat ||--o{ harga_modal_perhitungan : sumber_agregat
    harga_jual_obat ||--o{ harga_modal_perhitungan : jejak
    harga_jual_obat ||--o{ histori_harga_jual_obat : diarsipkan
```

### Alur data harga

```mermaid
flowchart LR
    Faktur["Faktur PBF\nharga_beli + diskon"] --> Detail["detail_pembelian\nnetto, modal, subtotal"]
    Detail --> Batch["stok_batch_obat\nharga_beli + harga_modal"]
    Detail --> Tagihan["tagihan_pbf\nnilai = DPP + PPN"]
    Batch --> ModalSvc["HargaModalService\nmodal_faktur + modal_agregat"]
    ModalSvc --> Konfirmasi["UI konfirmasi harga\nOTC ecer / REG·VIP kemasan"]
    Konfirmasi --> HJO["harga_jual_obat\n+ margin + satuan"]
    HJO --> Histori["histori_harga_jual_obat"]
    HJO --> Jejak["harga_modal_perhitungan"]
```

### Peran kolom kunci

| Tabel | Kolom harga utama | Catatan |
|-------|-------------------|---------|
| `detail_pembelian` | `harga_beli`, `harga_beli_netto`, `harga_modal`, `harga_modal_satuan`, `subtotal` | Snapshot per baris faktur; `subtotal` = DPP baris (tanpa PPN tagihan) |
| `stok_batch_obat` | `harga_beli`, `harga_modal` | Modal batch untuk agregat pricing |
| `pembelian` | `total_faktur` | **Incl. PPN 11%** atas Σ `subtotal` |
| `tagihan_pbf` | `nilai_faktur` | = `pembelian.total_faktur` |
| `harga_jual_obat` | `harga_otc/reg/vip`, `*_satuan`, `margin_*`, `harga_modal_terhitung`, `harga_modal_satuan` | 1 baris master **global** per obat (`cabang_id` null); DPB/stok tetap per cabang |
| `harga_modal_perhitungan` | `harga_modal`, `jumlah_stok`, `kontribusi` | Jejak batch saat simpan harga dari DPB |
| `histori_harga_jual_obat` | Snapshot harga lama | Append-only saat harga diganti |

---

## 4. Rules perhitungan

### 4.1 Per baris DPB (sebelum simpan)

```
bruto              = jumlah × harga_beli
subtotal (DPP)     = bruto − (bruto × diskon_persen / 100)
harga_beli_netto   = harga_beli × (1 − diskon_persen / 100)
harga_modal        = harga_beli_netto × 1.11
harga_modal_satuan = (harga_modal × 1.015) / isi_kemasan
```

Konstanta: `HargaPerhitunganService::PPN_MULTIPLIER = 1.11`, `MARKUP_SATUAN_MULTIPLIER = 1.015`.

### 4.2 Total tagihan faktur

```
subtotal_dpp = Σ detail.subtotal
nilai_ppn    = subtotal_dpp × 0.11
total_faktur = subtotal_dpp + nilai_ppn
```

- Form DPB: ringkasan live + **modal konfirmasi** sebelum submit.
- Method: `HargaPerhitunganService::hitungTotalFaktur()`, dipanggil dari `PembelianService`.

### 4.3 Modal agregat antar PBF (titik harga)

Sumber utama: riwayat `detail_pembelian` + `pembelian` di **cabang acuan** + obat.  
Stok batch (`jumlah_stok > 0`) dipakai untuk **FEFO / audit**, **bukan** bobot modal.

| Metode UI | Titik per PBF | Agregat antar PBF |
|-----------|---------------|-------------------|
| **Rata-rata PBF** (`rata_rata`) | `harga_modal` DPB **terbaru** PBF itu (`tanggal_terima` / id) | `(m1+…+mn) / n` |
| **Tertinggi** (`tertinggi`) | `harga_modal` **tertinggi** historis PBF itu | `max(m1…mn)` |

- Satu PBF = satu titik (pengiriman berulang tidak menambah slot).
- Tidak ada hard-cap yang memotong PBF; jika `n > 4` hanya **peringatan UI**.
- Fallback jika belum ada DPB: 1 titik dari `harga_modal_terhitung` aktif, atau batch `AWAL-*` berstok.

```
modal_faktur       = rata-rata tertimbang harga_modal baris DPB ini (info)
modal_agregat      = avg atau max dari titik PBF (di atas)
harga_markup       = modal_agregat × 1.015
harga_modal_satuan = harga_markup / isi_kemasan
```

**Dasar margin & OTC** = `harga_modal_satuan` dari **modal agregat**.  
**Acuan isi REG/VIP** = **modal agregat** (kemasan).

Contoh seeder (`SEED-MODAL-A*` / `B1`): PBF sama harga beda → titik = terbaru atau max dalam PBF; PBF beda obat sama → agregat mean/max antar PBF. Lihat `database/data/pembelian.json`.

### 4.4 Turunan harga jual (setelah admin input)

```
harga_otc_satuan = harga_otc                          // sudah ecer
harga_reg_satuan = harga_reg / isi_kemasan
harga_vip_satuan = harga_vip / isi_kemasan

margin_otc = (harga_otc_satuan − harga_modal_satuan) / harga_modal_satuan × 100
margin_reg = (harga_reg_satuan − harga_modal_satuan) / harga_modal_satuan × 100
margin_vip = (harga_vip_satuan − harga_modal_satuan) / harga_modal_satuan × 100
```

Preview UI dihitung **client-side (Alpine/JS)**; nilai final saat simpan dihitung ulang di **PHP** (`HargaJualService` + `HargaPerhitunganService`).

### 4.5 Warning margin VIP

```
warning jika margin_vip < 11   // persen, basis satuan
```

Tidak memblokir simpan.

### 4.6 Contoh angka (dari Excel A-010)

| Input / hasil | Nilai |
|---------------|-------|
| `harga_modal` (kemasan) | 40.721 |
| `harga_markup` | 41.332 |
| `isi_kemasan` | 10 |
| `harga_modal_satuan` | 4.133 |
| `harga_otc` (ecer) | 8.000 → margin ≈ 94% |
| `harga_reg` (kemasan) | 55.000 → satuan 5.500 → margin ≈ 33% |
| `harga_vip` (kemasan) | 52.000 → satuan 5.200 → margin ≈ 26% |

---

## 5. Alur operasional

### 5.1 Step 1 — Catat DPB

1. Admin isi header + detail (`harga_beli`, `diskon`, batch, expired, jumlah).
2. UI tampilkan subtotal per baris + ringkasan **DPP / PPN / total tagihan**.
3. Modal konfirmasi → submit.
4. `PembelianService::simpan()`:
   - hitung modal per baris, stok batch, mutasi;
   - `total_faktur` = DPP + PPN;
   - buat `tagihan_pbf`.
5. Redirect ke **`pembelian/{id}` (detail DPB)**. Review harga di **Manajemen Harga** (tab antrian); tombol konfirmasi per faktur tetap tersedia sebagai shortcut.

### 5.2 Step 2 — Penentuan harga jual

**Jalur utama — Manajemen Harga** (`/manajemen-harga`):

| Tab | Isi |
|-----|-----|
| `dpb_hari_ini` | Obat dari DPB `tanggal_terima` = hari ini (cabang acuan) |
| `modal_beda` | Modal DPB terbaru (round) ≠ `harga_modal_terhitung` aktif |
| `cari` | Search-first (`q` min 2); AJAX live-search hanya di tab ini |

**Jalur opsional — konfirmasi per faktur** (`pembelian/{id}/konfirmasi-harga`):

1. Tampilkan modal faktur vs modal agregat titik PBF (pilih metode).
2. Tampilkan **harga modal eceran** sebagai rujukan OTC.
3. Admin isi OTC (ecer), REG & VIP (kemasan).
4. JS hitung live: satuan REG/VIP + ketiga margin.
5. Submit → `HargaJualService::simpanHarga(..., 'dpb')`.

### 5.3 Edit / hapus DPB

Diizinkan hanya jika tagihan belum dibayar dan stok batch belum terpakai penjualan.  
Edit → reverse stok lama → tulis ulang → sync tagihan → redirect ke detail DPB (review harga opsional).

---

## 6. Service & file acuan

| Peran | Path |
|-------|------|
| Rumus (PPN unit, markup, total tagihan, margin) | `app/Services/HargaPerhitunganService.php` |
| Modal faktur + titik PBF / agregat | `app/Services/HargaModalService.php` |
| Antrian Manajemen Harga (tab) | `app/Services/ManajemenHargaService.php` |
| Simpan/update/hapus DPB + total PPN | `app/Services/PembelianService.php` |
| Tulis harga jual + arsip + jejak batch | `app/Services/HargaJualService.php` |
| Stok batch | `app/Services/StokService.php` |
| Controller | `app/Http/Controllers/PembelianController.php` |
| UI DPB + ringkasan/modal | `resources/views/pembelian/_form.blade.php` |
| UI konfirmasi harga (JS preview) | `resources/views/pembelian/konfirmasi-harga.blade.php` |
| Cursor rule singkat | `.cursor/rules/pricing-dpb.mdc` |
| Contoh Excel | `docs/contoh_tabel_harga.xlsx` |

---

## 7. Larangan

| Jangan | Karena |
|--------|--------|
| Membagi `harga_otc` dengan `isi_kemasan` | OTC sudah ecer |
| Menulis harga jual ke `master_obat` | Sudah di `harga_jual_obat` |
| Menyetarakan `total_faktur` = Σ subtotal tanpa PPN | Tagihan harus incl. PPN |
| Merata-ratakan `harga_modal` di DB | Agregat hanya di service |
| Auto-update harga jual saat DPB tanpa konfirmasi | Alur operasional dua langkah |
| Hard-block VIP &lt; 11% | Warning saja |

---

## 8. Status implementasi

| Fitur | Status |
|-------|--------|
| Istilah OTC ecer / REG·VIP kemasan | ✅ |
| Hitung `harga_modal` per baris & batch | ✅ |
| Total faktur = DPP + PPN 11% + modal ringkasan sebelum simpan | ✅ |
| Konfirmasi harga + preview JS | ✅ |
| `harga_modal_perhitungan` | ✅ |
| Edit/hapus DPB terbatas | ✅ |
| Warning margin VIP di konfirmasi | ✅ |
| Perbandingan harga beli lama vs baru di form DPB | ❌ |
| UI histori harga jual | ❌ |
| Import Excel harga bulanan | ❌ V0.3 |
