Skema Database & Relasi

Halaman ini merangkum seluruh tabel di database, kolom pentingnya, relasi antar model (persis seperti yang ditulis di app/Models/*.php), dan aturan bisnis yang menjaga datanya tetap konsisten. Tujuannya supaya kontributor baru tidak perlu membaca satu-satu file migrasi untuk memahami bagaimana data saling terhubung.

Aturan emas di aplikasi ini: jangan pernah mengubah saldo_santris atau menyisipkan baris transaksis secara langsung dari Livewire/Controller. Semua mutasi saldo wajib lewat app/Services/WalletService.php (credit() / debit()). Lihat bagian "Aturan Bisnis Penting" di bawah untuk daftar lengkap invarian serupa.

Peta Domain

27 tabel aplikasi (di luar tabel bawaan Spatie/Sanctum/framework seperti roles, personal_access_tokens, sessions) dikelompokkan menjadi 9 domain. Urutan di bawah kira-kira mengikuti alur data: dari identitas orang, ke struktur santri/keluarga, ke kartu & tagihan, sampai ke pergerakan uang.

1. Identitas & Akses      users, roles/permissions (Spatie), devices
2. Struktur Organisasi    lembagas, keluargas, santris, wali_santris
3. Kartu Santri           kartu_santris
4. Tagihan & Diskon      jenis_tagihans, kategori_diskons, periodes, tagihans, tagihan_pembayarans
5. Dompet (Ledger)        saldo_santris, transaksis
6. Top Up Wali (Midtrans) topup_walis
7. Penarikan Tunai        kebijakan_penarikans, penarikan_requests
8. Unit Usaha (Kantin)    unit_usahas, unit_usaha_transaksis, unit_usaha_penarikans,
                          unit_usaha_rekening_perubahans, kebijakan_kantins, kwitansis,
                          settings, banners, activity_log
9. Notifikasi Wali        wali_notifications

1. Identitas & Akses

users

KolomKeterangan
emailNullable — santri login pakai NIS, bukan email (lihat Auth/LoginForm).
nisDiisi hanya jika user ini adalah akun login santri (opsional, banyak santri belum tentu punya akun).
no_kkDiisi hanya untuk role wali — dipakai KeluargaLinkingService untuk auto-link ke semua santri dengan no_kk yang sama di tabel keluargas. Juga bisa dipakai sebagai identifier login (lihat "Login & Akun Wali Otomatis" di bawah). Boleh dipakai lebih dari satu user (satu keluarga bisa punya beberapa akun wali), tapi login by No. KK hanya berfungsi selama persis satu akun yang memakainya.
must_change_passwordBoolean, default false. Diset true saat akun wali dibuat otomatis dengan No. KK sebagai kata sandi awal (lihat WaliAccountService) — ditegakkan oleh middleware EnsurePasswordIsChanged, dibersihkan lagi begitu Profil::simpanPassword() berhasil.
pinNullable, di-cast hashed (bukan disimpan mentah, sama seperti password). PIN transaksi 6 digit untuk aksi yang memindahkan saldo lewat aplikasi mobile wali (bayar kantin, bayar tagihan dari saldo, transfer antar santri) — lihat PinService & User::hasPin(). Hanya wali yang memakainya saat ini; kolom ini bukan role-scoped di level database.
pin_set_atNullable, diisi ulang setiap kali PinService::set() dipanggil (pengaturan awal maupun ganti PIN) — timestamp murni informasional, tidak dipakai untuk logika apa pun saat ini.

Relasi: santri() hasOne (jika user ini akun login santri) · waliSantris() hasMany · anakAsuh() belongsToMany Santri lewat pivot wali_santris · unitUsahaDikelola() hasOne UnitUsaha (jika user ini akun pengelola kantin). Role (admin/bendahara/pengasuh/wali/santri/pengelola/dev) disimpan lewat package spatie/laravel-permission di tabel roles + model_has_roles, dicek dengan $user->hasRole() dan middleware role:xxx di route.

Lupa PIN diperlakukan sama seperti lupa kata sandi: tidak ada self-service reset. Admin mereset lewat halaman /admin/users (tombol "Reset PIN", menghapus pin/pin_set_at — lihat Admin\Users\Index::resetPin()), dan wali mengulang alur pengaturan PIN dari awal lewat aplikasi mobile.

Login & Akun Wali Otomatis

Auth/LoginForm menerima tiga bentuk identifier, dideteksi dari format inputnya: mengandung @ → email; persis 16 digit angka → no_kk; selain itu → nis. Untuk login by no_kk, sistem mengecek dulu berapa banyak user yang memakai No. KK itu — kalau bukan tepat satu (0 atau lebih dari 1), login ditolak sebagai kredensial salah, bukan mencoba menebak akun mana yang dimaksud.

WaliAccountService membuat akun wali "default" untuk sebuah keluarga (dari form Tambah Santri, halaman Data Keluarga, atau massal setelah Import Excel) dengan no_kk keluarga tsb sebagai login sekaligus kata sandi awal, dan must_change_password=true. Middleware EnsurePasswordIsChanged (di grup middleware web) mengunci user seperti ini ke halaman /profil untuk seluruh request kecuali ke /profil sendiri, /logout, dan request AJAX asli dari Livewire (dideteksi lewat header X-Livewire, bukan path endpoint-nya — endpoint Livewire sengaja diacak per-instalasi oleh package-nya sendiri, jadi tidak bisa dicocokkan dari path).

devices

Mesin kios (tapping kartu/fingerprint), autentikasi lewat Sanctum token (ability:kiosk), bukan lewat tabel users. tipe: kiosk_saldo, kiosk_penarikan, kantin. Relasi: penarikanRequests() hasMany — device mana yang memverifikasi fingerprint saat penarikan tunai.

2. Struktur Organisasi

lembagas

Unit pendidikan di bawah pondok (mis. MTs, MA). tipe: pondok_pusat, sekolah_formal, lainnya. Relasi: santris() hasMany, jenisTagihans() hasMany (jenis tagihan bisa spesifik per lembaga atau lembaga_id null = berlaku semua lembaga).

keluargas

Satu baris per Kartu Keluarga (no_kk unik). Selain nama_kepala_keluarga & alamat, ada biodata opsional kepala keluarga: nik_kepala_keluarga (16 digit, unik), tempat_lahir_kepala_keluarga, tanggal_lahir_kepala_keluarga — diisi saat keluarga baru dibuat (form Tambah Santri) atau diedit belakangan lewat halaman Data Keluarga, tidak pernah lewat proses lain.

Relasi: santris() hasMany — ini akar dari fitur "satu wali bisa punya banyak anak otomatis tertaut", karena KeluargaLinkingService mencocokkan users.no_kk dengan santris.keluarga_id → keluargas.no_kk. waliUsers() hasMany User (bukan foreign key sungguhan — dicocokkan lewat kolom no_kk yang sama, di-scope ke role wali saja), dipakai untuk menghitung "keluarga ini sudah/belum punya akun wali" di halaman Data Keluarga.

santris

Entitas utama sistem. status: baru → aktif → nonaktif|lulus|keluar (soft delete tersedia terpisah dari status ini). nis unik, nik nullable+unik (16 digit sesuai KTP/KIA — beda dari nis yang cuma nomor induk internal pondok), user_id nullable+unik (tidak semua santri diberi akun login).

KolomKeterangan
kategori_diskon_idNullable — diisi manual atau otomatis (lihat kategori_diskon_auto) oleh KategoriDiskonService saat santri baru pertama aktivasi, atau saat sinkronisasi "bersaudara" dalam satu keluarga.
kategori_diskon_autoPenanda kategori ini hasil auto-assign, bukan input manual admin — supaya re-sync tidak menimpa pilihan manual.

Relasi: keluarga(), kategoriDiskon(), user(), lembaga() semuanya belongsTo · kartuSantris() hasMany + kartuAktif() hasOne (kartu berstatus aktif saat ini) · waliSantris() hasMany + walis() belongsToMany User · tagihans(), transaksis(), penarikanRequests(), topupWalis() hasMany · saldo() hasOne ke saldo_santris.

wali_santris

Pivot many-to-many antara users (role wali) dan santris, unik per (user_id, santri_id). hubungan: ayah/ibu/wali/kerabat/lainnya. is_auto_generated membedakan baris hasil auto-link by KK (boleh dihapus/di-sync ulang oleh KeluargaLinkingService) dari baris yang ditambahkan manual admin lewat Admin/Wali/Index (harus dipertahankan). is_primary menandai wali utama untuk keperluan notifikasi.

3. Kartu Santri

kartu_santris

nomor_kartu unik (dicetak di kartu fisik), uid_kartu nullable+unik (UID chip RFID, diisi setelah tap-in di kios), fingerprint_template_ref hanya referensi opaque — data biometrik asli tidak pernah tersimpan di server, lihat FingerprintVerificationService. status: aktif, nonaktif, hilang, diblokir.

Relasi: santri() belongsTo, diaktifkanOleh() / dinonaktifkanOleh() belongsTo User.

Aturan penting: tidak ada aksi manual "nonaktifkan" di UI admin. SantriObserver::updated() otomatis menonaktifkan kartu aktif santri ketika status santri berubah menjadi nonaktif, lulus, atau keluar — lihat app/Observers/SantriObserver.php. Satu santri hanya boleh punya satu kartu berstatus aktif pada satu waktu (dicek di Admin/Kartu/Index::aktivasi(), bukan lewat constraint DB).

4. Tagihan & Diskon

jenis_tagihans

Template tagihan (mis. "SPP Bulanan", "Uang Makan"). periode: bulanan/tahunan/sekali. berlaku_diskon menentukan apakah kategori diskon santri dipakai saat generate. Relasi: lembaga() belongsTo (nullable), tagihans() hasMany.

kategori_diskons

Mis. "Yatim 50%", "Bersaudara 20%". persentase (0–100). Relasi: santris() hasMany. Dikelola lewat app/Services/KategoriDiskonService.php, termasuk logika "sinkronkan kategori bersaudara" saat ada santri baru aktif dalam satu keluarga (lihat SantriObserver).

periodes

Label periode penagihan/laporan (mis. 2026-07). Hanya boleh ada satu is_active=true pada satu waktu — ditegakkan oleh Periode::activate() lewat DB::transaction(). Periode::syncExpired() otomatis menonaktifkan periode yang tanggal_selesai-nya sudah lewat, dipanggil di beberapa entry point (mis. mount() Laporan Keuangan).

tagihans

Unique (santri_id, jenis_tagihan_id, periode_label) — ini yang membuat TagihanService::generateTagihanForPeriode() aman dijalankan berkali-kali untuk periode yang sama tanpa membuat duplikat (idempotent by database constraint, bukan cuma cek aplikasi).

KolomKeterangan
nominalNominal final (setelah diskon) — ini yang dipakai untuk perhitungan tagihan berjalan.
nominal_sebelum_diskonNullable. Hanya diisi kalau diskon benar-benar diterapkan (lihat TagihanService::hitungDiskon()). Kalau null, artinya nominal = nominal kotor (tidak ada diskon).
diskon_persen, kategori_diskon_idSnapshot kategori diskon & persentase pada saat tagihan dibuat — sengaja disalin (bukan cuma join ke santris.kategori_diskon_id) supaya histori tagihan lama tidak berubah kalau kategori diskon santri berubah di kemudian hari.
generated_batch_idUUID yang sama untuk semua tagihan dari satu kali proses generate — dipakai untuk audit "batch mana yang menghasilkan tagihan ini".

Relasi: santri(), jenisTagihan(), generatedBy() (User), kategoriDiskon() semuanya belongsTo · pembayarans() hasMany.

tagihan_pembayarans

Riwayat cicilan/pelunasan satu tagihan (satu tagihan bisa dibayar bertahap). sumber: tunai_langsung (dicatat manual admin, tidak menyentuh saldo), saldo (potong saldo_santris lewat WalletService::debit(), punya baris transaksis pasangan — kolom transaksi_id unik), transfer_wali_tagihan (wali membayar tagihan ini langsung via Midtrans tanpa lewat saldo, lihat topup_wali_id & topup_walis.tagihan_id). transfer_wali_otomatis legacy — sisa dari perilaku top up-otomatis-melunasi-tagihan yang sudah dihapus, hanya ada di baris lama, tidak pernah ditulis kode baru lagi. Relasi: tagihan(), transaksi() (nullable), topupWali() (nullable), dicatatOleh() semuanya belongsTo.

5. Dompet (Ledger) — inti sistem

saldo_santris

Satu baris per santri (santri_id sebagai primary key, bukan id auto-increment terpisah), hanya kolom saldo + updated_at. Sengaja dipisah dari tabel santris supaya row-lock saat transaksi keuangan (lockForUpdate()) tidak pernah bentrok dengan edit profil biodata santri yang sering terjadi terpisah. Relasi: santri() belongsTo.

transaksis — ledger, immutable

Setiap baris adalah satu pergerakan saldo yang tidak pernah diubah atau dihapus setelah dibuat — Transaksi model menolak update()/delete() di boot() dengan melempar ImmutableLedgerException. Model ini juga tidak punya updated_at (const UPDATED_AT = null) karena baris ledger memang tidak pernah diperbarui.

KolomKeterangan
jenistopup_tunai, topup_transfer_wali, penarikan_tunai, pembayaran_tagihan, penyesuaian, pembayaran_kantin, transfer_antar_santri.
arahdebit (saldo berkurang) atau kredit (saldo bertambah).
saldo_sebelum / saldo_sesudahSnapshot saldo tepat sebelum & sesudah baris ini — sehingga histori bisa direkonstruksi/diaudit tanpa perlu replay seluruh ledger dari awal.
idempotency_keyUnik, nullable — lapisan pengaman kedua (di luar unique constraint tabel sumber seperti midtrans_order_id) supaya proses yang retry/replay tidak pernah membuat baris ledger ganda.
referensi_type / referensi_idPolymorphic (referensi() morphTo) — menunjuk ke apa yang memicu transaksi ini: PenarikanRequest (penarikan_tunai), UnitUsaha (pembayaran_kantin — kantin mana yang dibayar), atau Santri lain (transfer_antar_santri — santri di sisi seberang transfer; setiap baris menunjuk ke lawan transaksinya, bukan ke baris pasangannya sendiri, karena ledger immutable tidak bisa saling menunjuk id sebelum keduanya dibuat).

Satu-satunya jalan masuk resmi ke tabel ini adalah app/Services/WalletService.php:

  • credit(Santri $santri, int $nominal, string $jenis, array $attrs = [])
  • debit(Santri $santri, int $nominal, string $jenis, array $attrs = []) — melempar InsufficientBalanceException sebelum menulis apa pun kalau saldo tidak cukup.

Keduanya membungkus DB::transaction() + lockForUpdate() pada baris saldo_santris yang bersangkutan, supaya dua request yang datang bersamaan (mis. tap kios dobel) tidak pernah menghasilkan saldo yang salah.

Penting: transaksis/saldo_santris BUKAN kas pondok. Jumlah saldo_santris adalah kewajiban (liability) pondok ke santri/wali — uang yang dititipkan, bukan pendapatan pondok. Untuk posisi kas pondok sendiri (uang fisik di kasir + saldo Midtrans), lihat app/Services/LegerKasPondokService.php (halaman /admin/leger-kas-pondok) — ini sepenuhnya turunan (derived) dari transaksis (jenis topup_tunai & penarikan_tunai), topup_walis (status paid, nilai penuh nominal_diminta sebagai kas masuk, plus biaya_midtrans sebagai kas keluar terpisah kalau biaya_ditanggung_wali=false — lihat di bawah), dan tagihan_pembayarans (sumber tunai_langsung) — tidak ada tabel baru, supaya tidak ada risiko dua sumber kebenaran yang bisa tidak sinkron. pembayaran_tagihan (bayar dari saldo) dan penyesuaian sengaja dikecualikan karena tidak ada uang fisik yang berpindah saat itu terjadi.

6. Top Up Wali (Midtrans)

topup_walis

Satu permintaan top up dari wali lewat Midtrans, dibuat via Snap (createSnapTransaction()) atau Core API (createCoreApiTransaction()) — midtrans_order_id unik jadi kunci idempotensi webhook untuk keduanya. status: pending → paid|expired|failed|cancelled|refunded. tagihan_id nullable: kosong untuk top up biasa (selalu 100% ke saldo), terisi untuk top up yang dibuat lewat createSnapTransactionForTagihan() khusus melunasi satu tagihan tanpa menyentuh saldo. nominal_potongan_tagihan + nominal_ke_saldo mencatat bagaimana nominal_diminta dipecah (lihat aturan bisnis di bawah). raw_notification menyimpan payload webhook/charge mentah untuk audit/debug.

snap_token/redirect_url hanya terisi untuk transaksi via Snap. payment_type (bni_va/qris), va_bank, va_number, qr_url, expiry_time hanya terisi untuk transaksi via Core API — keduanya nullable karena satu baris hanya pernah dibuat lewat salah satu jalur.

biaya_midtrans (default 0) & biaya_ditanggung_wali (default false) — biaya transaksi Midtrans yang dihitung & dikunci saat charge dibuat oleh chargeCoreApi(), dari jadwal biaya yang admin atur di /admin/pengaturan/midtrans (MidtransFeeService). Hanya terisi untuk jalur Core API — jalur Snap selalu 0/false karena channel pembayarannya baru diketahui setelah notifikasi Midtrans datang, sudah terlambat untuk dihitung di muka. Tidak pernah mengurangi nominal_diminta/saldo santri — kalau biaya_ditanggung_wali=true, biayanya ditambahkan ke gross_amount yang di-charge ke Midtrans (wali bayar lebih); kalau false, wali bayar nominal_diminta apa adanya dan biayanya jadi beban kas pondok (lihat entri "Biaya Admin Midtrans" di LegerKasPondokService). Nilai pada satu baris adalah snapshot kebijakan saat itu — mengubah pengaturan biaya nanti tidak mengubah baris yang sudah ada.

Relasi: user() (wali pemohon), santri() belongsTo · tagihanPembayarans() hasMany (tagihan-tagihan yang terlunasi otomatis dari top up ini).

Dikelola oleh app/Services/TopupWaliService.php: createSnapTransaction(), createCoreApiTransaction(), handleWebhook() (verifikasi signature sha512, cek status terminal, row-lock) — sama untuk transaksi dari kedua jalur — dan syncStatusFromMidtrans() (dipakai tombol "Cek Status Sekarang" untuk development lokal karena webhook Midtrans tidak bisa menjangkau localhost — lihat halaman Instalasi & Kebutuhan).

7. Penarikan Tunai

kebijakan_penarikans

Aturan jam operasional (jam_mulai/jam_selesai) dan limit_harian per santri, opsional dibatasi ke satu applies_lembaga_id. Bisa ada beberapa baris is_active berbeda lembaga; effective_from menandai kapan kebijakan ini mulai berlaku.

penarikan_requests

status: menunggu → disetujui → selesai, atau ditolak/dibatalkan. dalam_jam_kebijakan, melebihi_limit_harian, wajib_surat_keterangan dihitung & disimpan saat permintaan dibuat (bukan dihitung ulang tiap kali dilihat), supaya keputusan sesuai kondisi kebijakan di momen itu. transaksi_id nullable+unik, hanya terisi setelah fulfill() berhasil membuat baris transaksis.

Relasi: santri(), device() (kios yang memverifikasi fingerprint), diprosesOleh() (User), transaksi() semuanya belongsTo.

Dikelola oleh app/Services/PenarikanService.php: createRequest() (cek kebijakan) → reviewSuratKeterangan() (jika wajib) → approve()/reject() → fulfill() (satu-satunya tempat yang boleh memanggil WalletService::debit() untuk jenis penarikan_tunai).

8. Unit Usaha (Kantin/Koperasi)

unit_usahas

Satu baris per unit usaha (kantin/koperasi) pondok. kode unik — inilah yang di-encode ke QR code yang dipindai wali di aplikasi mobile untuk membayar (lihat UnitUsahaController::show() di API Wali). saldo_unit adalah saldo/pendapatan unit usaha ini — ledgernya sengaja terpisah dari saldo_santris/transaksis karena unit usaha bukan santri (lihat unit_usaha_transaksis di bawah). pengelola_user_id nullable: satu akun dengan role pengelola mengelola paling banyak satu unit usaha (User::unitUsahaDikelola()), dibuatkan lewat PengelolaAccountService (pola sama seperti WaliAccountService) dari halaman /admin/kantin. Kolom bank_nama/bank_no_rekening/bank_atas_nama menyimpan rekening tujuan pencairan saldo unit usaha.

Relasi: pengelola() belongsTo User · transaksis(), penarikans(), rekeningPerubahans() hasMany.

unit_usaha_transaksis — ledger, immutable

Ledger unit usaha, mengikuti konvensi immutable yang sama seperti transaksis (updating()/deleting() melempar ImmutableLedgerException, tanpa kolom updated_at). jenis: pembayaran_masuk (dari pembayaran kantin santri — transaksi_id menunjuk baris transaksis pasangannya, dibuat atomik lewat KantinPembayaranService::bayar()) atau penarikan_keluar (pencairan saldo unit usaha — unit_usaha_penarikan_id menunjuk baris unit_usaha_penarikans yang memicunya). Dikelola oleh app/Services/UnitUsahaWalletService.php, pola credit()/debit() yang sama seperti WalletService.

unit_usaha_penarikans

Permintaan pencairan saldo unit usaha oleh pengelola ke rekening bank yang terdaftar. status: menunggu → disetujui → selesai, atau ditolak — alur persetujuannya sama seperti penarikan_requests milik santri (pengelola mengajukan lewat /pengelola/penarikan, admin/bendahara yang menyetujui & mencairkan lewat /admin/kantin/penarikan). referensi_transfer mencatat nomor referensi transfer bank manual yang dilakukan petugas saat mencairkan. Dikelola oleh app/Services/UnitUsahaPenarikanService.php.

unit_usaha_rekening_perubahans

Pengajuan ganti rekening bank tujuan pencairan oleh pengelola (/pengelola/rekening) — tidak langsung menimpa unit_usahas.bank_*, harus disetujui admin/bendahara dulu lewat /admin/kantin/rekening (status: menunggu/disetujui/ditolak), supaya tujuan pencairan berikutnya tidak bisa dialihkan sepihak oleh pengelola tanpa sepengetahuan pondok. Dikelola oleh app/Services/UnitUsahaRekeningService.php.

kebijakan_kantins

Batas belanja kantin harian (limit_harian) per santri, opsional dibatasi ke satu applies_lembaga_id — struktur & cara kerja persis mirip kebijakan_penarikans (domain 7) tapi untuk pembayaran kantin, bukan penarikan tunai; sengaja tabel terpisah (bukan reuse) karena keduanya melindungi hal yang beda: satu membatasi tunai yang bisa ditarik, satu membatasi non-tunai yang bisa dibelanjakan di kantin. Diperiksa oleh KantinPembayaranService::bayar() sebelum saldo didebit; tanpa kebijakan is_active, belanja kantin tidak dibatasi harian.

kwitansis

Kwitansi resmi bernomor permanen (nomor_kwitansi, format KWT-{tahun}-{6 digit}) — diterbitkan otomatis, tepat sekali, oleh KwitansiService saat pembayaran tagihan (TagihanService::applyPembayaran(), ketiga sumbernya) atau pembayaran kantin (KantinPembayaranService::bayar()) berhasil. jenis: tagihan atau kantin. tagihan_pembayaran_id dan transaksi_id sama-sama nullable karena tidak semua kombinasi berlaku: pembayaran tagihan tunai_langsung hanya punya tagihan_pembayaran_id (tidak ada baris transaksis sama sekali untuk sumber ini), sedangkan kwitansi kantin hanya punya transaksi_id. dicetak_oleh/dicetak_at nullable & hanya terisi saat staf mencetak ulang lewat /admin/kwitansi/{kwitansi}/cetak — penerbitan otomatis tidak pernah mengisi kedua kolom ini.

Penomoran aman dari duplikat di bawah pembayaran bersamaan tanpa row-lock tambahan: baris disisipkan dulu dengan UUID sementara (memenuhi constraint NOT NULL+unique), baru ditulis ulang dengan nomor final begitu id barisnya sendiri diketahui — mengandalkan jaminan auto-increment database, bukan hitungan MAX(id)+1 sebelum insert seperti KartuSantri::nomorKartuBerikutnya() (yang aman karena penerbitan kartu jarang bersamaan; penerbitan kwitansi terjadi di setiap pembayaran, jadi butuh jaminan lebih kuat).

Diekspos ke aplikasi mobile lewat tautan PDF bertanda tangan (GET /api/wali/kwitansi/{kwitansi} → URL::temporarySignedRoute, 15 menit) — lihat API Wali. Menggunakan template PDF yang sama dengan struk informal (InvoiceService), dibedakan lewat judul "KWITANSI RESMI" dan catatan permanensi nomornya.

TabelKeterangan
settingsKey-value generik (value di-cast encrypted, selalu string — tidak ada array/JSON, satu key per field). Saat ini dipakai untuk menyimpan kredensial Midtrans (terenkripsi) yang diatur admin lewat /admin/pengaturan/midtrans, batas minimum saldo (SaldoFloorService, key tagihan_minimal_saldo_setelah_bayar), jadwal biaya Midtrans (MidtransFeeService, halaman yang sama — key midtrans_biaya_dibebankan_wali ("1"/"0") plus satu pasang midtrans_biaya_{bni_va|bca_va|bri_va|qris}_tipe ("tetap"/"persen") & _nilai per channel, 9 key total, semua default kosong/nol sampai admin mengisinya), dan branding aplikasi (AppSettingsService — app_nama_aplikasi, app_nama_pondok, app_alamat, app_telepon, app_email, dan app_logo_path untuk logo yang diunggah admin lewat /admin/pengaturan/aplikasi, disimpan di disk public) — semuanya terpisah dari .env.
bannersBanner carousel di layar Home aplikasi mobile wali (pengumuman/promosi, mis. ajakan donasi/hibah wali ke pesantren). Dikelola admin lewat /admin/banner. gambar_path disimpan di disk public (sama seperti app_logo_path), aktif menentukan tampil/tidaknya, urutan menentukan posisi di carousel kalau lebih dari satu banner aktif. Diekspos publik (tanpa token) lewat GET /api/wali/banners — lihat API Wali. File gambar otomatis terhapus dari storage saat baris ini dihapus (Banner::booted(), event deleting).
wali_notificationsPusat notifikasi persisten per wali. Menyimpan judul, isi, tipe, payload JSON, dan read_at. Baris dibuat oleh PushNotificationService sebelum upaya kirim FCM, sehingga pesan tetap tersedia saat perangkat offline atau token FCM tidak ada. Relasi ke users memakai cascade delete dan endpoint API selalu memverifikasi kepemilikan wali.
activity_logDari package spatie/laravel-activitylog — audit trail otomatis untuk perubahan pada model-model penting.
devices (Sanctum)Token kios disimpan di personal_access_tokens standar Sanctum, terhubung ke model Device (bukan User) sebagai tokenable.

Aturan Bisnis Penting (invarian yang wajib dijaga)

  1. Ledger immutable. Tidak ada jalan untuk UPDATE/DELETE baris transaksis — koreksi selalu berupa baris baru jenis=penyesuaian, bukan mengubah baris lama.
  2. Mutasi saldo hanya lewat WalletService. Jangan pernah SaldoSantri::query()->update(...) langsung dari Livewire/Controller.
  3. Generate tagihan idempoten by design. Unique constraint (santri_id, jenis_tagihan_id, periode_label) di database, bukan cuma pengecekan di kode — aman dijalankan ulang.
  4. Kartu tidak pernah dinonaktifkan manual. Otomatis lewat SantriObserver saat status santri berubah ke nonaktif/lulus/keluar.
  5. Auto-link wali by no_kk tidak menimpa link manual. KeluargaLinkingService hanya menyentuh baris wali_santris dengan is_auto_generated=true.
  6. Webhook Midtrans idempoten. Unique midtrans_order_id + verifikasi signature sebelum akses DB apa pun + short-circuit kalau status sudah terminal (paid/expired/dst).
  7. Top up wali selalu 100% masuk ke saldo — tidak ada lagi pemotongan otomatis untuk tagihan. TopupWaliService::settle(): kalau topup_walis.tagihan_id kosong (top up biasa), seluruh nominal_diminta di-credit() ke saldo. Membayar tagihan adalah aksi terpisah yang wali pilih sendiri di halaman Bayar Tagihan, dengan dua opsi: dari saldo (TagihanService::bayarDariSaldo()), atau langsung via Midtrans untuk satu tagihan spesifik (TopupWaliService::createSnapTransactionForTagihan(), mengisi topup_walis.tagihan_id dan nominal_diminta = tagihan->sisa() persis) — pembayaran jenis ini tidak pernah menyentuh saldo sama sekali, kecuali tagihannya keburu lunas lewat kanal lain sebelum webhook datang, baru sisanya di-credit() ke saldo (lihat settleTagihanScoped()).
  8. Biaya Midtrans tidak pernah mengurangi apa yang diterima santri. topup_walis.biaya_midtrans/biaya_ditanggung_wali murni memengaruhi gross_amount yang di-charge ke Midtrans (dan pencatatan kas pondok) — tidak pernah nominal_diminta/nominal_ke_saldo/nominal_potongan_tagihan. Fitur ini opt-in: sebelum admin mengisi jadwal biaya di /admin/pengaturan/midtrans, semua channel bernilai 0 dan kebijakannya "ditanggung pondok", jadi perilaku sistem persis sama seperti sebelum fitur ini ada.
  9. Batas minimum saldo (SaldoFloorService::minimal(), default 100.000, admin-editable lewat /admin/pengaturan/midtrans, key tagihan_minimal_saldo_setelah_bayar) ditegakkan konsisten di tiga alur pemindahan saldo yang diinisiasi wali dari aplikasi mobile: bayar tagihan dari saldo (TagihanService::bayarDariSaldo()), bayar kantin (KantinPembayaranService::bayar()), dan transfer antar santri (TransferSaldoService::transfer()) — ketiganya melempar SaldoDiBawahMinimumException dengan pola cek yang sama: lock saldo dalam transaksi, lalu tolak hanya jika saldo cukup untuk membayar tapi hasilnya jatuh di bawah batas (kalau saldo memang tidak cukup sama sekali, InsufficientBalanceException dari WalletService::debit() yang menang duluan — dua fakta berbeda yang wali perlu tahu). Penarikan tunai fisik (PenarikanService) dan pemotongan tagihan langsung via Midtrans tidak tunduk pada batas ini — keduanya bukan aksi "sisakan uang jajan" yang batas ini dimaksudkan untuk melindungi.
  10. PIN transaksi (bukan kata sandi akun) menggerbangi ketiga alur pemindahan saldo di atas dari aplikasi mobile. WaliApiController::requirePin() memanggil PinService::verify(), yang mengunci verifikasi (423 Locked, lewat PinLockedException) selama 15 menit setelah 5 kali percobaan salah berturut-turut (RateLimiter facade, bukan kolom counter di database — pola yang sama seperti Kios\CekSaldo). Wali yang belum pernah mengatur PIN (users.pin masih null) tidak bisa lewat gerbang ini sampai mengatur PIN lebih dulu lewat PinController::store().
  11. Pengurus tidak bisa mengorigin penarikan. Baris transaksis berjenis penarikan_tunai hanya boleh lahir dari PenarikanRequest::fulfill() yang sudah melalui approve().
  12. Akun keluarga/Santri/User tidak pernah overwrite data yang sudah ada. Admin/Santri/Form, halaman Data Keluarga, dan SantriImport semuanya cek dulu apakah no_kk sudah terdaftar sebelum menulis — kalau sudah ada, data keluarga yang ada dipakai apa adanya (tidak pernah updateOrCreate menimpa nama_kepala_keluarga/biodata lain).
  13. Role harus di-assign sebelum auto-link wali disinkronkan ulang. UserObserver::created() memicu KeluargaLinkingService::syncForUser() tepat saat baris users disisipkan — tapi assignRole('wali') baru bisa jalan setelah baris itu ada, jadi sync pertama itu tidak pernah menemukan apa-apa. WaliAccountService dan Admin/Users/Index::save() keduanya memanggil ulang syncForUser() secara eksplisit setelah role terpasang untuk menutup celah ini (lihat komentar di kode terkait sebelum mengubah alur pembuatan user).
  14. Perubahan rekening pencairan unit usaha harus disetujui, tidak pernah langsung. Pengajuan pengelola lewat /pengelola/rekening masuk ke unit_usaha_rekening_perubahans berstatus menunggu dan tidak menyentuh unit_usahas.bank_* sampai admin/bendahara menyetujuinya — mencegah pengelola mengalihkan tujuan pencairan tanpa sepengetahuan pondok.

Mau lihat skema live dari database yang sedang jalan?

Halaman ini ditulis manual supaya bisa disertai penjelasan & aturan bisnis — untuk kolom/tipe data paling akurat saat ini (termasuk migrasi terbaru yang mungkin belum sempat didokumentasikan di sini), jalankan php artisan db:table <nama_tabel>, atau lihat langsung file migrasi di database/migrations/.