### Spec

## Roles
- Admin
- Reseller

## Data
- topup
- nasabah (punya reseller) (nama, no hp, alamat)
- no rek. nasabah (anggep aja 1 nasabah bisa punya beberapa no rek) (nama bank / wallet, no rek/no wallet)
- setoran / gaji / potongan (jadi 1 tabel) (perlu diskusi lebih lanjut)

## Flow

### Setup Masterdata
- admin input reseller
- reseller input nasabah dan no rek masing2 nasabah
- nasabah akan di filter per reseller (sesuai login)

### Flow Top up
- Topup di lakukan oleh reseller dengan memilih nasabah dan nominal
- Top up hanya bisa di lakukan di hari minggu - jum'at
- Rekap/summary top up per minggu (tgl pakai hari sabtu)

### Setoran / Payment
- admin akan input setoran /payment (hanya akan terjadi di tgl yg hari sabtu)
- rekap/summary di ambil per mingguan
- ada jumlah top up(summary topup) dan jumlah payment (uang yg harus disetor kan ((summary topup * 20% + summary top up)))
- salary juga sama masih di table ini, hitungannya jumlah payment - summary top up * 50%
- jumlah yg di setorkan = jumlah payment
- kalo jumlah yg di setorkan kurang / ada sisa ada 2 jalur, sisa itu auto masuk ke setoran top up minggu depan (atas nama reseller itu sendiri / pinjaman reseller (hitungannya beda lagi)) 
- untuk pinjaman reseller / karyawan akan di potong per minggu dengan bunga 10% tiap minggu. 

### Riwayat setoran bertahap (payment_entries)
- Reseller bayar **bertahap** (nyicil), bukan sekali setor. Tiap cicilan = 1 baris `payment_entries` (`status`: PENDING/VALIDATED/REJECTED, `source`: RESELLER/ADMIN).
- `weekly_settlements.paid_amount` jadi **turunan** = Σ entri **VALIDATED** (bukan input manual).
- **Reseller** (menu `/setoran`, read-only daftar settlement sendiri): tombol Bayar → entri **PENDING** (belum jadi uang masuk). Estimasi sisa = `total_due − tervalidasi − menunggu validasi`. Reseller hanya boleh **hapus** entri miliknya yang masih PENDING (tidak ada edit).
- **Admin** (menu payment): **validasi/tolak** entri PENDING reseller, **tambah** setoran (langsung VALIDATED), **hapus** entri. Tiap aksi → `paid_amount` dihitung ulang.
- **Overpay ditolak**: reseller tidak boleh > estimasi sisa; admin tidak boleh bikin Σ VALIDATED > `total_due`.
- **Tutup minggu** (admin) = aksi terpisah: kunci gaji/potongan/bonus + aksi sisa → status `SETTLED` (baru bisa cetak slip gaji).
- **SETTLED = terkunci**: tambah/validasi/tolak/hapus pembayaran diblok sampai admin **reset ke draft** dulu (di UI kontrol disembunyikan + banner). Reseller juga tidak bisa bayar minggu yang sudah ditutup.
- Command `backfill:payment-entries` (idempoten): bikin entri VALIDATED untuk data `paid_amount` lama.

### Potongan gaji untuk bayar setoran (salary_to_payment)
- Selain `deduction` (potongan gaji → cicilan **pinjaman**), ada `salary_to_payment` (potongan gaji → bayar **setoran**). Field di `weekly_settlements`, diisi admin di Tutup Minggu.
- Efek: `remaining = total_due − paid_amount − salary_to_payment` dan `net_salary = salary − deduction − salary_to_payment + bonus`.
- **Cap**: `deduction + salary_to_payment ≤ gaji` dan `salary_to_payment ≤ sisa setoran` (anti-overpay).
- **UI**: di **form** Tutup Minggu dipisah jadi dua field ("Potongan Pinjaman" + "Potongan Setoran"); di **kolom tabel** & slip gaji digabung jadi satu "Potongan" (= deduction + salary_to_payment). Info "Sisa (real)" di modal admin & reseller sudah dikurangi salary_to_payment.
- Hapus/update = ubah angka di Tutup Minggu (0 = tidak ada); minggu SETTLED → reset ke draft dulu.

### Carry-in (sisa setoran dibawa ke minggu depan)
- `carry_in` (sisa kurang setor minggu lalu, jalur CARRY) **diperlakukan sebagai topup** minggu ini — cuma tidak masuk history/list topup.
- Dasar hitung `base = summary_topup + carry_in`. Jadi carry_in **ikut kena margin 20%** dan **ikut jadi dasar gaji**:
  - yg harus disetor (`total_due`) = `base * 1.2`
  - gaji = `base * 20% * salary_share_percent`
- **Aksi sisa default = CARRY** (admin tidak wajib milih; override ke LOAN bila mau jadi pinjaman).
- Carry **di-trigger hanya saat tutup minggu**: sisa minggu N mengalir ke N+1 hanya kalau minggu N **sudah `SETTLED`** (dengan aksi CARRY & remaining > 0). Minggu yang masih DRAFT/berjalan tidak pernah carry walau sudah ada setoran sebagian.

### Rekap (per minggu, hari sabtu)
- **Total Modal** = Σ nominal topup minggu itu **+ Σ carry_in** (carry_in dianggap topup).
- **Total Payment** = Σ `paid_amount` (setoran riil saja; carry_in TIDAK dihitung karena bukan uang masuk).
- **Gaji** = Σ `net_salary`.
- **Untung** = Total Payment − Total Modal − Gaji.
- **Total Uang** = Total Modal + Untung (≡ Total Payment − Gaji).

### INput pinjaman
- admin dapat input pinjaman dari sisa payment / input baru
- dibayar / potong per minggu
- intinya (sisa pinjaman * 10%) + sisa pinjaman 
- kalo ada pinjaman baru akan summary lagi
- **Shortfall LOAN terhitung MINGGU DEPAN**: sisa setoran minggu N (mis. 27 Jun) yang dilempar ke pinjaman dibuat sebagai `LoanEntry` `source=SHORTFALL` ditanggal **minggu N+1** (mis. 4 Jul), bukan minggu N. Konsekuensi: bunga 10% baru jalan dari N+1. (Pinjaman MANUAL tetap ditanggal sesuai input.)
- **Potongan gaji bisa menyicil shortfall minggu depan**: saat tutup minggu N, potongan pinjaman pertama menyicil **pinjaman lama** minggu N (`oldRoom = dueBeforePayment(N) − pembayaran manual minggu N`); kelebihannya (`toNew = deduction − oldRoom`) menyicil **pinjaman shortfall N+1**. Cap potongan = `oldRoom + shortfallDue` (`shortfallDue = sisa × 1.1`). Jadi walau reseller **belum punya pinjaman lama**, gaji minggu N tetap bisa memotong pinjaman baru yang lahir N+1. `toNew` dicatat sebagai `LoanPayment` `source=DEDUCTION` di minggu N+1 (idempoten, disinkronkan tiap tutup minggu).
- **Riwayat pembayaran DEDUCTION = read-only**: baris `LoanPayment` `source=DEDUCTION` (dari potongan gaji) tidak bisa diedit/dihapus manual di layar pinjaman (UI tanpa tombol + label "terkunci"/badge "otomatis", controller `editPayment`/`delete` di-filter `source=MANUAL`). Berubah/hilang **hanya** lewat **reset settlement → tutup ulang** (potongan diubah/di-nol-in → `syncDeductionPayment` menyesuaikan). Pembayaran MANUAL tetap bebas diedit/hapus.
- **Total pinjaman (pokok/bunga/sisa)**: `LoanLedger::totals()` = **dekomposisi sisa pinjaman TERKINI** (`outstanding` = `currentOutstanding()`) jadi `principal = outstanding / 1,1` dan `interest = outstanding − principal`, jadi **bunga selalu tepat 10% dari pokok** (cara hitung owner) dan `principal + interest == outstanding`. BUKAN total seumur hidup. (Definisi lama "bayar bunga dulu baru pokok / pokok-asli vs bunga-berbunga" sudah **diganti** commit `80ca2b8` — akurat tapi bunga bukan 10% pokok.) Grand total lintas reseller ditampilkan di **menu Pinjaman** (3 kartu: Total Pinjaman Pokok + Total Bunga + Total Sisa Pinjaman, ikut filter pencarian) & di **Dashboard admin** (lihat bawah). Dihitung dari ledger sampai minggu berjalan (shortfall N+1 baru ikut saat minggunya tiba).
- **Dashboard admin 3 section** (grid tiap section pilih jumlah kolom yang **membagi rata** jumlah kartu — kelipatan 4→4, 3→3, 2→2 — biar baris terakhir tak timpang): (1) **Minggu berjalan** (6 kartu, 3+3) = Total Reseller / Total Nasabah / Pinjaman Aktif (baris atas) + Topup Minggu Ini / **Total Payment** (Σ `paid_amount` minggu berjalan) / **Total Gaji Net** (Σ `net_salary` minggu berjalan) (baris bawah); (2) **Pinjaman Seller** = Pokok Berjalan / Bunga Pinjaman Berjalan / Total Pinjaman (Σ `totals()`); (3) **Ringkasan Keuangan** = Saldo Cashflow + Saldo Tabungan Seller. Dashboard reseller tetap 2 section (Minggu berjalan + Ringkasan Keuangan: Sisa Pinjaman + Saldo Tabungan).


## UIX
- di menu payment admin, langsung ambil per mingguan dengan 
- di payment selain input setoran, input juga potongan dan bisa cetak slip gaji
- di menu top up reseller hanya append only, kalo request edit / hapus di admin aja
- admin bisa lihat nasabah, filter per reseller.


