# Database

## Ringkasan

- **Engine:** MySQL 8.4 (produksi/dev) atau SQLite (testing)
- **Konvensi tabel master:** prefix `master_`
- **Konvensi tabel transaksi:** nama Indonesia singular/plural sesuai domain (`pembelian`, bukan `purchases`)
- **Foreign keys:** eksplisit di migration, `constrained('master_*')`
- **Enum values:** string Bahasa Indonesia (status, jenis, sumber kas)
- **Timestamps:** `created_at`, `updated_at` di semua tabel domain

---

## Diagram relasi (ERD)

```mermaid
erDiagram
    master_cabang ||--o{ pembelian : receives
    master_cabang ||--o{ penjualan : sells_at
    master_cabang ||--o{ stok_batch_obat : stocks
    master_cabang ||--o{ tagihan_pbf : owes
    master_cabang ||--o{ rencana_pembayaran_pbf : plans
    master_cabang ||--o{ mutasi_stok : tracks

    master_pbf ||--o{ pembelian : supplies
    master_pbf ||--o{ tagihan_pbf : invoices
    master_pbf ||--o{ rencana_pembayaran_pbf : payment_plan

    master_obat ||--o{ detail_pembelian : purchased
    master_obat ||--o{ detail_penjualan : sold
    master_obat ||--o{ stok_batch_obat : batched
    master_obat ||--o{ harga_jual_obat : priced
    master_obat }o--|| master_golongan_obat : classified
    master_obat }o--|| master_kategori_obat : categorized
    master_obat }o--|| master_satuan_obat : unit

    master_pelanggan ||--o{ penjualan : buys

    pembelian ||--|{ detail_pembelian : contains
    detail_pembelian ||--o| stok_batch_obat : creates
    pembelian ||--o| tagihan_pbf : generates
    tagihan_pbf ||--o{ pembayaran_tagihan_pbf : dibayar
    pembayaran_tagihan_pbf }o--o| mutasi_kas : optional_kas
    pembelian ||--o{ harga_jual_obat : triggered_by

    harga_jual_obat ||--o{ histori_harga_jual_obat : archived
    harga_jual_obat ||--o{ harga_modal_perhitungan : basis
    stok_batch_obat ||--o{ harga_modal_perhitungan : source_batch

    penjualan ||--|{ detail_penjualan : contains
    detail_penjualan ||--|{ detail_penjualan_batch : consumes
    detail_penjualan_batch }o--|| stok_batch_obat : from_batch

    stok_batch_obat ||--o{ mutasi_stok : audited
```

---

## Tabel master

### `master_cabang`

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `nama_cabang` | string | Nama apotek/cabang |
| `alamat` | string nullable | |
| `telepon` | string nullable | |
| `keterangan` | text nullable | |

**Model:** `App\Models\Cabang`

### `master_pbf`

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `nama_pbf` | string | Nama supplier |
| `alamat` | string nullable | |
| `telepon` | string nullable | |
| `kontak_sales` | string nullable | |
| `keterangan` | text nullable | |

**Model:** `App\Models\Pbf`  
**Seeder:** `PbfSeeder` — baca `database/data/pbf.json` (dari `seeder.xlsx` sheet PBF)  
**Key upsert:** `nama_pbf`

### `master_pelanggan`

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `kode_pelanggan` | string unique | |
| `nama_pelanggan` | string | |
| `nama_faktur` | string nullable | Nama di faktur |
| `area` | string nullable | |
| `top` | integer | Term of payment (hari) |
| `jenis_harga` | enum | `OTC`, `REG`, `VIP` |
| `alamat` | text nullable | |
| `nomor_hp` | string nullable | |
| `keterangan` | text nullable | |

**Model:** `App\Models\Pelanggan`  
**Seeder:** `PelangganSeeder` — baca `database/data/pelanggan.json` (dari `seeder.xlsx` sheet PELANGGAN)  
**Key upsert:** `kode_pelanggan`

---

### `master_obat`

Identitas obat saja (V0.2a). Harga jual dipisah ke `harga_jual_obat`.

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `kode_obat` | string unique | Kode produk. Auto-generate saat create dari aplikasi (`KodeObatGenerator`); import Excel memakai kode dari file |
| `nama_obat` | string | |
| `golongan_obat_id` | FK nullable | → `master_golongan_obat` |
| `kategori_obat_id` | FK nullable | → `master_kategori_obat` |
| `satuan_obat_id` | FK nullable | → `master_satuan_obat` (kemasan terkecil) |
| `isi_kemasan` | unsigned int | Jumlah isi per satuan (mis. 10 tablet per strip) |
| `keterangan` | text nullable | |

**Catatan:**
- Tidak ada `stok_awal`, `harga_modal`, atau kolom harga jual di master.
- Stok aktual ada di `stok_batch_obat`; harga beli per batch di `detail_pembelian` / `stok_batch_obat`.
- Harga jual OTC/REG/VIP ada di `harga_jual_obat` (satu record aktif per obat/cabang).
- Label UI **Kemasan Terkecil** memetakan ke `satuan_obat_id`.
- **Auto-generate `kode_obat`:** prefix = huruf pertama `nama_obat` (fallback `X`); nomor = max existing + 1 per prefix; format `{PREFIX}-{NNN}` (min 3 digit). Lihat `docs/business-rules.md` § Auto-generate kode obat.

**Model:** `App\Models\Obat`  
**Service:** `App\Services\KodeObatGenerator` (generate kode saat create)  
**Relasi penting:** `satuanObat()`, `hargaJual()`, accessor virtual `satuan`

### Dictionary obat (3 tabel)

Pola identik untuk `master_golongan_obat`, `master_kategori_obat`, `master_satuan_obat`:

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `kode` | string(50) unique | Kode singkat, contoh: `ANALGESIK`, `STRIP` |
| `nama` | string | Label tampilan |
| `deskripsi` | text nullable | |
| `urutan` | smallint nullable | Urutan tampil |
| `is_active` | boolean default true | |

**Models:** `GolonganObat`, `KategoriObat`, `SatuanObat`  
**Trait:** `IsMasterDictionary` → scope `active()`, `ordered()`

**Catatan `master_satuan_obat`:**
- Merupakan kamus satuan/kemasan terkecil obat (STRIP, BOTOL, PCS, TABLET, dll.).
- Tabel `master_bentuk_sediaan` dan `master_satuan_obat` lama telah digabung; hanya `master_satuan_obat` yang dipakai.
- Nilai baru dari Excel (`KEMASAN TERKECIL`) dapat auto-insert via `MasterDictionaryResolver` saat seeding.

---

## Tabel harga jual obat (V0.2a / V0.2b)

> Istilah & rumus: [`pricing-dpb-rules.md`](pricing-dpb-rules.md).  
> Relasi: `master_obat` → `harga_jual_obat` → `histori_harga_jual_obat` / `harga_modal_perhitungan` ← `stok_batch_obat`.

### `harga_jual_obat`

Harga jual master per obat — **selalu global** (`cabang_id` null, berlaku semua cabang). **Satu baris aktif per obat**; `berlaku_selesai` selalu null pada baris master. DPB/stok tetap per cabang.

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `obat_id` | FK | → `master_obat` |
| `cabang_id` | FK nullable | Null = harga global semua cabang |
| `harga_otc` | decimal(15,2) | Harga jual **ecer** (pelanggan umum) |
| `harga_reg` | decimal(15,2) | Harga jual **kemasan utuh** (medis REG) |
| `harga_vip` | decimal(15,2) | Harga jual **kemasan utuh** (medis VIP) |
| `harga_otc_satuan` | decimal(15,4) nullable | = `harga_otc` (sudah ecer) |
| `harga_reg_satuan` | decimal(15,4) nullable | `harga_reg / isi_kemasan` |
| `harga_vip_satuan` | decimal(15,4) nullable | `harga_vip / isi_kemasan` |
| `margin_otc` | decimal(5,2) nullable | % vs `harga_modal_satuan` |
| `margin_reg` | decimal(5,2) nullable | % vs `harga_modal_satuan` |
| `margin_vip` | decimal(5,2) nullable | % vs `harga_modal_satuan` |
| `harga_modal_terhitung` | decimal(15,2) nullable | Modal agregat **kemasan** (rata-rata / tertinggi) |
| `harga_modal_satuan` | decimal(15,4) nullable | `(modal_agregat × 1.015) / isi` — dasar margin & acuan OTC |
| `metode_harga_modal` | string(20) nullable | `rata_rata`, `tertinggi` |
| `berlaku_mulai` | date | Tanggal mulai berlaku |
| `berlaku_selesai` | date nullable | Null = harga aktif |
| `sumber` | string(20) | `manual`, `sistem`, `dpb`, dll. |
| `pembelian_id` | FK nullable | DPB pemicu jika `sumber = dpb` |
| `dasar_perhitungan` | text nullable | Catatan perhitungan |

**Model:** `App\Models\HargaJualObat`  
**Service:** `HargaJualService::simpanHarga()` — selalu menulis `cabang_id = null`; mode `versi` arsip ke histori lalu UPDATE; mode `koreksi` UPDATE tanpa histori; insert hanya jika baris belum ada. Cleanup: `php artisan harga:cleanup-global`.

### `histori_harga_jual_obat`

Arsip perubahan harga saat harga aktif diganti.

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `harga_jual_obat_id` | FK | Record yang diarsipkan |
| `obat_id` | FK | |
| `cabang_id` | FK nullable | |
| `harga_otc`, `harga_reg`, `harga_vip` | decimal(15,2) | Snapshot harga |
| `harga_modal_terhitung` | decimal(15,2) nullable | |
| `metode_harga_modal` | string(20) nullable | |
| `margin_otc`, `margin_reg`, `margin_vip` | decimal(5,2) nullable | |
| `sumber` | string(20) | |
| `pembelian_id` | FK nullable | |
| `berlaku_mulai`, `berlaku_selesai` | date | Periode berlaku harga lama |
| `catatan` | text nullable | |
| `created_at` | timestamp | Waktu pengarsipan |

**Model:** `App\Models\HistoriHargaJualObat`

### `harga_modal_perhitungan`

Jejak batch sumber `harga_modal_terhitung` saat simpan harga dari DPB.

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `harga_jual_obat_id` | FK | |
| `stok_batch_obat_id` | FK | |
| `harga_modal` | decimal(15,2) | Modal batch saat perhitungan |
| `jumlah_stok` | unsigned int | Stok batch saat perhitungan |
| `kontribusi` | decimal(15,4) nullable | Bobot rata-rata |

**Model:** `App\Models\HargaModalPerhitungan`

---

## Tabel transaksi — pembelian

### `pembelian`

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `tanggal_terima` | date | Tanggal barang diterima |
| `tanggal_faktur` | date | Tanggal faktur PBF |
| `pbf_id` | FK | → `master_pbf` |
| `cabang_id` | FK | → `master_cabang` |
| `nomor_faktur` | string | |
| `total_faktur` | decimal(15,2) | **Σ subtotal DPP + PPN 11%** (nilai tagihan) |
| `status_tagihan` | string | Default `Belum Lunas` |
| `keterangan` | text nullable | |

**Accessor model:** `subtotal_dpp` (Σ `detail.subtotal`), `nilai_ppn` (`subtotal_dpp × 0.11`).

### `detail_pembelian`

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `pembelian_id` | FK | |
| `obat_id` | FK | → `master_obat` |
| `kode_produk` | string | Snapshot dari master |
| `nama_barang` | string | Snapshot |
| `sk` | string nullable | Snapshot isi kemasan |
| `nomor_batch` | string | **Wajib** di validasi form |
| `tanggal_expired` | date | **Wajib** di validasi form |
| `jumlah` | integer | Jumlah kemasan |
| `harga_beli` | decimal(15,2) | Harga faktur per kemasan (sebelum diskon) |
| `harga_beli_netto` | decimal(15,2) | Setelah diskon, sebelum PPN — untuk modal unit |
| `harga_modal` | decimal(15,2) | Modal **kemasan** = `harga_beli_netto × 1.11` |
| `harga_modal_satuan` | decimal(15,4) | Modal **ecer** dari modal baris ini |
| `subtotal` | decimal(15,2) | DPP baris = `jumlah × harga_beli − diskon` (tanpa PPN tagihan) |
| `diskon_persen` | decimal nullable | |

---

## Tabel stok

### `stok_batch_obat`

Stok aktual per kombinasi cabang + obat + batch + expired.

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `cabang_id` | FK | |
| `obat_id` | FK | |
| `pembelian_detail_id` | FK nullable | Sumber pembelian |
| `nomor_batch` | string | |
| `tanggal_expired` | date | |
| `jumlah_stok` | unsigned int | Sisa stok batch |
| `harga_beli` | decimal(15,2) | Harga beli mentah dari faktur (audit) |
| `harga_modal` | decimal(15,2) | Modal kemasan dari DPB — sumber agregat pricing |
| `stok_minimum` | unsigned int default 10 | Ambang peringatan |
| `keterangan` | text nullable | |

**Index:** `(cabang_id, obat_id, nomor_batch, tanggal_expired)` — nama `stok_batch_lookup`

**Merge rule:** Jika DPB ulang dengan kombinasi identik, `jumlah_stok` di-increment (bukan duplikat record).

### `mutasi_stok`

Audit trail pergerakan stok.

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `tanggal` | date | |
| `cabang_id` | FK | |
| `obat_id` | FK | |
| `stok_batch_obat_id` | FK nullable | Batch terkait |
| `jenis_mutasi` | enum/string | `Pembelian`, `Penjualan`, `Koreksi`, `Retur` |
| `jumlah` | integer | Positif = masuk, negatif = keluar |
| `sumber_transaksi` | string nullable | `pembelian`, `penjualan`, `koreksi_stok` |
| `referensi_id` | bigint nullable | ID transaksi sumber |
| `keterangan` | text nullable | |

---

## Tabel transaksi — penjualan

### `penjualan`

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `tanggal_penjualan` | date | |
| `jenis_penjualan` | enum | `Medis`, `OTC` |
| `cabang_id` | FK | |
| `pelanggan_id` | FK nullable | Wajib untuk Medis |
| `nomor_faktur` | string nullable | Medis: `{YYMMDD}F-{NNN}`; OTC: kode internal `OTC-{YYMMDD}-{NNN}` |
| `tanggal_jatuh_tempo` | date nullable | Medis: diisi manual per faktur |
| `total_penjualan` | decimal(15,2) | |
| `metode_pembayaran` | string nullable | `CASH` / `TRANSFER` |
| `sumber_kas` | enum nullable | Force: CASH Medis=`Kas Medis`, CASH OTC=`Kas OTC`, TRANSFER=`BCA` |
| `keterangan` | text nullable | |

### `detail_penjualan`

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `penjualan_id` | FK | |
| `obat_id` | FK | |
| `kode_barang` | string | Snapshot |
| `nama_barang` | string | Snapshot |
| `jumlah` | integer | |
| `satuan` | string nullable | Snapshot dari master |
| `harga_satuan` | decimal(15,2) | Snapshot tier harga |
| `diskon` | unsigned int | Nominal Rupiah, default 0 |
| `subtotal` | decimal(15,2) | `max(0, jumlah×harga − diskon)` |

### `detail_penjualan_batch`

Jejak batch mana yang dikonsumsi (FEFO traceability).

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `detail_penjualan_id` | FK | |
| `stok_batch_obat_id` | FK | |
| `jumlah` | integer | Jumlah dari batch ini |

---

## Tabel keuangan

### `tagihan_pbf`

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `pbf_id` | FK | |
| `cabang_id` | FK | |
| `pembelian_id` | FK nullable | Sumber DPB |
| `nomor_faktur` | string | |
| `tanggal_faktur` | date | |
| `tanggal_terima` | date | |
| `tanggal_jatuh_tempo` | date | Auto: faktur + 30 hari |
| `nilai_faktur` | decimal(15,2) | |
| `jumlah_bayar` | decimal(15,2) | Kumulatif |
| `sisa_tagihan` | decimal(15,2) | |
| `status_tagihan` | enum | `Belum Lunas`, `Sebagian`, `Lunas` |
| `keterangan` | text nullable | |

### `pembayaran_tagihan_pbf`

Jejak per peristiwa bayar (histori). Agregat tetap di `tagihan_pbf.jumlah_bayar` / `sisa_tagihan`.

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `tagihan_id` | FK → `tagihan_pbf` | Cascade delete |
| `tanggal_bayar` | date | |
| `jumlah_bayar` | decimal(15,2) | Nominal peristiwa ini |
| `sumber_dana` | enum nullable | `Kas Medis`, `Kas OTC`, `BCA` |
| `sisa_setelah` | decimal(15,2) | Snapshot sisa tagihan setelah peristiwa |
| `mutasi_kas_id` | FK nullable → `mutasi_kas` | Null jika bayar tanpa sumber dana |

### `rencana_pembayaran_pbf`

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `tanggal_rencana` | date | |
| `pbf_id` | FK | |
| `cabang_id` | FK | |
| `jumlah_rencana_bayar` | decimal(15,2) | |
| `sumber_dana` | enum | `Kas Medis`, `Kas OTC`, `BCA` |
| `status` | enum | `Direncanakan`, `Dibayar`, `Batal` |
| `keterangan` | text nullable | |

**Catatan:** Rencana pembayaran **tidak** otomatis mengeksekusi pembayaran tagihan.

### `mutasi_kas`

| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | bigint PK | |
| `tanggal` | date | |
| `sumber_kas` | enum | `Kas Medis`, `Kas OTC`, `BCA` |
| `cabang_id` | FK nullable | Jejak cabang (penjualan/manual/PBF dari laci). Null = global (mis. PBF dari BCA) |
| `jenis_mutasi` | enum | `masuk`, `keluar` |
| `kategori` | string nullable | Contoh: `Penjualan OTC`, `Pembayaran PBF` |
| `keterangan` | text nullable | |
| `pemasukan` | decimal(15,2) | |
| `pengeluaran` | decimal(15,2) | |
| `saldo` | decimal(15,2) | Running balance **global per sumber_kas** (cara tulis baris). UI Kas Tunai memakai neto per `cabang_id`, bukan kolom ini. |

---

## Tabel Laravel default

| Tabel | Penggunaan |
|-------|------------|
| `users` | Auth admin |
| `sessions` | Session driver database |
| `cache`, `cache_locks` | Cache store database |
| `jobs`, `job_batches`, `failed_jobs` | Queue (belum dipakai aktif) |
| `password_reset_tokens` | Belum dipakai (fitur lupa password tidak ada) |

---

## Seeder & data awal

Setelah `php artisan migrate --seed`:

| Entitas | Jumlah | Sumber / contoh |
|---------|--------|-----------------|
| User | 1 | admin@apoteksiaga.test |
| Cabang | 2 | Apotek Siaga 24, 25 (hardcoded di `DatabaseSeeder`) |
| PBF | 23 | `database/data/pbf.json` (dari `seeder.xlsx` sheet PBF) |
| Obat | ~2205 | `database/data/obat.json` (dari `seederObat.xlsx`) |
| Pelanggan | 62 | `database/data/pelanggan.json` (dari `seeder.xlsx` sheet PELANGGAN) |
| Dictionary obat | ~38+ | Golongan, kategori, satuan (auto-expand saat seed obat) |
| Pembelian (DPB) | 3 faktur | `database/data/pembelian.json` → `pembelian`, `detail_pembelian`, `stok_batch_obat`, `mutasi_stok`, `tagihan_pbf`; 2 faktur juga set `harga_jual_obat` + `harga_modal_perhitungan` |

**Pipeline seed master data:**

```bash
# PBF + pelanggan (satu file Excel)
python scripts/generate-pbf-pelanggan-data.py   # seeder.xlsx → pbf.json + pelanggan.json

# Obat (file Excel terpisah)
python scripts/generate-obat-data.py            # seederObat.xlsx → obat.json

# Jalankan semua seeder
php artisan migrate --seed
```

**Urutan di `DatabaseSeeder`:** User → Cabang → `PbfSeeder` → `MasterObatDictionarySeeder` → `ObatSeeder` → `PelangganSeeder` → `PembelianSeeder`

### Sample DPB (`pembelian.json`)

`PembelianSeeder` memakai `PembelianService` + `HargaJualService` (bukan insert mentah), sehingga total faktur sudah termasuk PPN 11% dan harga jual mengikuti aturan OTC ecer / REG·VIP kemasan.

| `nomor_faktur` | Cabang | Konfirmasi harga | Catatan |
|----------------|--------|------------------|---------|
| `SEED-DPB-001` | Siaga 24 | Ya | Multi-item, metode `rata_rata` |
| `SEED-DPB-002` | Siaga 25 | Ya | 2 batch obat sama, metode `tertinggi` |
| `SEED-DPB-003` | Siaga 24 | Tidak | Menunggu konfirmasi harga di UI |

Jalankan ulang hanya DPB: `php artisan db:seed --class=PembelianSeeder` (idempotent by `nomor_faktur`).

### Mapping Excel → `master_pbf` (sheet PBF)

| Kolom Excel | Kolom DB |
|-------------|----------|
| NAMA SUPPLIER | `nama_pbf` |
| ALAMAT | `alamat` |
| NO TELP | `telepon` |
| KETERANGAN | `keterangan` |

Contoh supplier: SERASI DARMA PUTERA, HARUM SENTOSA, COMBI PUTRA, ANUGERAH PHARMINDO LESTARI.

### Mapping Excel → `master_pelanggan` (sheet PELANGGAN)

| Kolom Excel | Kolom DB |
|-------------|----------|
| KODE PELANGGAN | `kode_pelanggan` |
| NAMA PELANGGAN | `nama_pelanggan` |
| NAMA DI FAKTUR | `nama_faktur` |
| AREA | `area` |
| TOP | `top` |
| HARGA | `jenis_harga` (`OTC`, `REG`, `VIP`) |
| ALAMAT | `alamat` |
| NO HP | `nomor_hp` |
| KETERANGAN | `keterangan` |

**Distribusi data seed pelanggan saat ini:** 40 REG, 22 VIP (penjualan Medis). Area umum: BARAT, TIMUR, APT.

### Mapping Excel → `master_obat` (`seederObat.xlsx`)

| Kolom Excel | Kolom DB |
|-------------|----------|
| KODE OBAT | `kode_obat` (dipertahankan dari Excel, tidak auto-generate) |
| NAMA OBAT | `nama_obat` |
| KATEGORI OBAT | `kategori_obat_id` |
| KEMASAN TERKECIL | `satuan_obat_id` |
| ISI KEMASAN TERKECIL | `isi_kemasan` |

---

## Migration history penting

| Tanggal | Migration | Perubahan |
|---------|-----------|-----------|
| 2026-06-30 | 000001–000014 | Skema domain lengkap V0 |
| 2026-07-03 | 000001 | Tambah 4 tabel dictionary obat |
| 2026-07-03 | 000002 | Migrasi kolom text → FK di `master_obat` |
| 2026-07-03 | 000003 | Rename `cabang`→`master_cabang`, dll. |
| 2026-07-07 | 000001–000002 | Tabel `harga_jual_obat`, `histori_harga_jual_obat` |
| 2026-07-07 | 000003 | Pindah harga jual dari `master_obat`; `isi_kemasan` jadi integer |
| 2026-07-07 | 000004 | Gabung `master_satuan_obat` lama ke dictionary bentuk |
| 2026-07-09 | 000001–000002 | Kolom `harga_modal*`; tabel `harga_modal_perhitungan` |

Saat menambah migration baru, **selalu** uji kompatibilitas MySQL dan SQLite.
