API Wali

REST API untuk aplikasi mobile wali santri. Semua endpoint mengembalikan JSON. Base URL mengikuti APP_URL di server, mis. https://keuangan.pesantren-latee.test/api.

Endpoint ini terpisah dari portal web wali (/wali/*, berbasis session Livewire) — API ini stateless dan didesain untuk dikonsumsi aplikasi mobile native.

Kontrak tipe JSON lintas hosting

Semua ID, saldo, nominal, sisa, dan persentase harus berupa JSON number; flag harus boolean; data harus array; field wajib tidak boleh null. Normalisasi dilakukan di Laravel Resources karena PHP/PDO/MySQL pada provider berbeda dapat mengembalikan BIGINT/DECIMAL sebagai string. Parser mobile tetap menerima angka/string untuk kompatibilitas mundur. Lihat prosedur uji dan diagnosis lengkap di Deployment & Mitigasi Hosting.

Info Aplikasi (Publik)

Satu-satunya endpoint di API ini yang benar-benar tidak butuh apa pun — tidak token, tidak login sama sekali. Dipakai aplikasi mobile untuk menampilkan branding (logo, nama aplikasi, nama pondok) di layar splash dan login, yaitu sebelum sesi apa pun ada. Datanya diambil langsung dari AppSettingsService, sumber yang sama dipakai portal web (lihat halaman Skema Database, tabel settings) — ubah lewat /admin/pengaturan/aplikasi, otomatis kepakai di web dan mobile.

Info aplikasi

GET /api/wali/app-info

200 OK

{
  "nama_aplikasi": "Sistem Keuangan Santri",
  "nama_pondok": "Pondok Pesantren Latee (Annuqayah)",
  "logo_url": "https://keuangan.pesantren-latee.test/storage/logo/abc123.png"
}

logo_url adalah null kalau admin belum pernah mengunggah logo — aplikasi mobile diharapkan fallback ke aset logo lokal yang dibundel di dalam aplikasi sendiri, bukan menampilkan gambar rusak. Nilai lain (nama_aplikasi/nama_pondok) selalu terisi (ada default bawaan di AppSettingsService kalau admin belum pernah mengatur apa pun).

Banner Beranda (Publik)

Sama seperti info aplikasi di atas — tidak butuh token. Dipakai carousel banner pengumuman/promosi di layar Home aplikasi mobile (mis. pengumuman pondok, ajakan donasi/hibah wali). Dikelola admin lewat /admin/banner, model App\Models\Banner (lihat Skema Database). Hanya baris aktif=true yang dikembalikan, terurut sesuai kolom urutan.

List banner aktif

GET /api/wali/banners

200 OK

{
  "data": [
    {
      "id": 3,
      "judul": "Ajakan Donasi Renovasi Asrama",
      "gambar_url": "https://keuangan.pesantren-latee.test/storage/banners/abc123.jpg",
      "link_url": "https://wa.me/6281234567890"
    }
  ]
}

link_url adalah null kalau admin tidak mengisi tautan — aplikasi mobile menampilkan banner sebagai gambar statis (tidak bisa disentuh) dalam kondisi itu. Kalau tidak ada banner aktif sama sekali, data adalah array kosong — aplikasi mobile diharapkan menyembunyikan seluruh bagian carousel (bukan menampilkan area kosong): tepat satu banner aktif tampil penuh lebar, dua atau lebih tampil sebagai carousel dengan banner berikutnya sedikit terlihat di tepi kanan.

Autentikasi

Memakai Laravel Sanctum personal access token (Bearer token), bukan session/cookie. Setiap token dibuat dengan ability wali — token ini tidak bisa dipakai untuk endpoint lain (mis. endpoint kiosk internal) dan sebaliknya.

Akun wali dibuat oleh admin/petugas pondok (tidak ada self-registration). Hubungi pondok jika wali belum punya akun. Sebagian akun dibuat otomatis oleh admin dengan No. KK sebagai login sekaligus kata sandi awal (lihat WaliAccountService di halaman Skema Database) — API ini mendukung alur itu, lihat field login dan endpoint Ubah Kata Sandi di bawah.

Login

POST /api/wali/login

FieldTipeWajibKeterangan
loginstringyaEmail akun wali, atau No. KK (16 digit angka). Dideteksi otomatis dari formatnya: mengandung @ → dicocokkan ke email; persis 16 digit → dicocokkan ke No. KK. Login by No. KK hanya berhasil kalau persis satu akun memakai No. KK itu — kalau ambigu (0 atau >1 akun), ditolak sebagai kredensial salah.
passwordstringyaKata sandi. Untuk akun yang baru dibuat otomatis, ini sama dengan No. KK-nya sendiri.
device_namestringyaNama perangkat, mis. "iPhone 15 - Budi". Dipakai sebagai label token per perangkat.

Contoh request (login by email)

curl -X POST https://keuangan.pesantren-latee.test/api/wali/login \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"login":"wali@pesantren.test","password":"password","device_name":"iPhone 15 - Budi"}'

Contoh request (login by No. KK, akun baru/default)

curl -X POST https://keuangan.pesantren-latee.test/api/wali/login \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"login":"3529010101010001","password":"3529010101010001","device_name":"iPhone 15 - Budi"}'

200 OK

{
  "token": "3|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "user": {
    "id": 12,
    "name": "Abdurrahman",
    "email": "wali@pesantren.test",
    "phone": "081234567890",
    "must_change_password": false
  }
}

Kalau must_change_password bernilai true, aplikasi mobile wajib mengarahkan wali ke layar ganti kata sandi (lihat endpoint Ubah Kata Sandi di bawah) sebelum membiarkan mereka memakai fitur lain — ini mencerminkan perilaku wajib-ganti-password di portal web (EnsurePasswordIsChanged middleware) untuk akun yang kata sandi awalnya masih No. KK.

422 Unprocessable Entity — login/password salah, atau akun bukan akun wali

{
  "message": "Email/No. KK atau kata sandi salah.",
  "errors": { "login": ["Email/No. KK atau kata sandi salah."] }
}

Simpan token di secure storage (Keychain/Keystore). Kirim di setiap request berikutnya:

Authorization: Bearer 3|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json

Login dibatasi 6 percobaan per menit per IP (throttle bawaan Laravel).

Login yang berhasil hanya membuktikan endpoint autentikasi sehat. Setelah login, aplikasi melakukan request terpisah ke /wali/anak, lalu endpoint saldo, tagihan, dan transaksi. Setiap endpoint tersebut wajib masuk smoke test deployment.

Logout

POST /api/wali/logout butuh token

Mencabut token yang sedang dipakai (perangkat lain tetap aktif).

200 OK

{ "message": "Berhasil keluar." }

Profil

GET /api/wali/me butuh token

200 OK

{ "id": 12, "name": "Abdurrahman", "email": "wali@pesantren.test", "phone": "081234567890", "must_change_password": false }

Ubah Profil

PUT /api/wali/profile butuh token

Setara dengan form data diri di portal web (Profil::simpanProfil()). Hanya name, email, phone yang bisa diubah wali sendiri — nis/no_kk tidak ada di endpoint ini (tidak pernah dikirim ke wali lewat API) karena keduanya hanya bisa diubah admin.

FieldTipeWajibKeterangan
namestringyaMaks 255 karakter.
emailstringtidakHarus email valid & belum dipakai akun lain.
phonestringtidakMaks 50 karakter.

200 OK

{ "id": 12, "name": "Abdurrahman", "email": "wali@pesantren.test", "phone": "081234567890", "must_change_password": false }

422 — email sudah dipakai akun lain, atau validasi lain gagal

{
  "message": "The email has already been taken.",
  "errors": { "email": ["The email has already been taken."] }
}

Ubah Kata Sandi

POST /api/wali/password butuh token

Setara dengan form "Ubah Kata Sandi" di portal web (Profil::simpanPassword()) — dipakai untuk memenuhi must_change_password tanpa perlu berpindah ke web. Begitu berhasil, must_change_password otomatis jadi false.

FieldTipeWajibKeterangan
current_passwordstringyaKata sandi saat ini (untuk akun baru, ini No. KK-nya).
passwordstringyaKata sandi baru, minimal 8 karakter.
password_confirmationstringyaHarus sama persis dengan password.

200 OK

{ "message": "Kata sandi berhasil diubah." }

422 — current_password salah, atau password baru tidak memenuhi aturan

{
  "message": "Kata sandi saat ini salah.",
  "errors": { "current_password": ["Kata sandi saat ini salah."] }
}

PIN Transaksi

PIN transaksi adalah lapis keamanan kedua khusus untuk aksi yang memindahkan saldo santri: Bayar kantin, Bayar tagihan dari saldo, dan Transfer (lihat bagian masing-masing di bawah) — terpisah dari kata sandi akun, supaya ponsel yang sedang tidak terkunci tidak otomatis jadi satu-satunya penghalang buat memindahkan saldo. Ketiga endpoint aksi tsb mewajibkan field pin (string 6 digit) di body request, divalidasi lewat PinService.

Wali belum tentu sudah mengatur PIN. Cek dulu lewat Status PIN sebelum menampilkan form aksi yang butuh PIN — kalau has_pin: false, arahkan wali ke alur pengaturan PIN dua langkah: Konfirmasi Kata Sandi dulu, baru kalau berhasil tampilkan form Atur PIN (jangan minta kata sandi & PIN sekaligus dalam satu form — kalau kata sandinya salah, wali baru tahu di akhir setelah mengisi semuanya).

Status PIN

GET /api/wali/pin/status butuh token

200 OK

{ "has_pin": true }

Konfirmasi Kata Sandi

POST /api/wali/pin/confirm-password butuh token

Memverifikasi kata sandi akun sungguhan di server, tanpa efek samping apa pun (tidak mengubah/menghapus PIN yang sudah ada) — langkah pertama dari alur pengaturan PIN dua tahap di aplikasi mobile.

FieldTipeWajibKeterangan
passwordstringyaKata sandi akun saat ini.

200 OK

{ "message": "Kata sandi benar." }

422 — kata sandi salah

{
  "message": "The given data was invalid.",
  "errors": { "password": ["Kata sandi salah."] }
}

Atur PIN

POST /api/wali/pin butuh token

Mengatur PIN baru, atau menimpa PIN yang sudah ada — tidak ada endpoint terpisah untuk "ganti PIN". Mewajibkan current_password sendiri (independen dari endpoint Konfirmasi Kata Sandi di atas, yang hanya untuk UX progresif di langkah pertama) — guard yang sama seperti AuthController::password() sebelum mengganti kredensial apa pun.

FieldTipeWajibKeterangan
current_passwordstringyaKata sandi akun saat ini.
pinstringyaPersis 6 digit angka.
pin_confirmationstringyaHarus sama persis dengan pin.

200 OK

{ "message": "PIN transaksi berhasil disimpan." }

422 — current_password salah, pin bukan 6 digit, atau pin_confirmation tidak cocok

{
  "message": "Kata sandi salah.",
  "errors": { "current_password": ["Kata sandi salah."] }
}

Kalau wali lupa PIN, tidak ada endpoint self-service reset — sama seperti lupa kata sandi, wali harus menghubungi admin pondok untuk mereset PIN lewat portal admin (/admin/users, tombol “Reset PIN”). Setelah direset, has_pin kembali false dan wali mengulang alur pengaturan PIN dari awal.

PIN terkunci sementara

Setiap endpoint yang memvalidasi pin (bayar kantin, bayar tagihan dari saldo, transfer — bukan endpoint di bagian ini) mengunci verifikasi PIN wali selama 15 menit setelah 5 kali percobaan salah berturut-turut, lalu otomatis terbuka lagi. Satu percobaan yang benar mereset hitungan ke nol. Status ini per-wali (bukan per-endpoint), jadi 5 percobaan salah di bayar kantin ikut mengunci transfer & bayar tagihan juga.

423 Locked

{ "message": "Terlalu banyak percobaan PIN salah. Coba lagi dalam 15 menit." }

Format Error

Semua error mengikuti format standar Laravel:

StatusKapan terjadi
401Token tidak ada / tidak valid / sudah dicabut
403Token valid tapi mencoba mengakses santri yang tidak tertaut dengan akun wali tsb (atau token dengan ability yang salah)
404Resource tidak ditemukan (mis. tagihan_id yang bukan milik santri_id di path)
422Validasi gagal, atau aksi ditolak oleh aturan bisnis (mis. saldo tidak cukup, Midtrans belum dikonfigurasi admin)
{ "message": "Ringkasan error." }

Untuk 422 validasi, ada tambahan field errors (map nama-field → array pesan), format standar Laravel validation.

Konsep: Tidak Ada “Switch Akun” di API

Portal web menyimpan “anak aktif” di session (fitur switch akun). API tidak memakai konsep ini — setiap request yang menyangkut santri tertentu menyertakan {santri} (ID santri) langsung di path URL. Ini lebih cocok untuk mobile (stateless, mendukung multi-anak sekaligus di satu layar tanpa perlu “switch” dulu).

Setiap endpoint yang menerima {santri} di path selalu diverifikasi bahwa santri tsb benar tertaut ke wali yang sedang login (lewat penautan No. KK otomatis atau tautan manual oleh admin). Jika tidak tertaut → 403.

Daftar Anak (Santri)

List semua anak yang tertaut

GET /api/wali/anak butuh token

Jika wali punya lebih dari satu santri di bawah No. KK yang sama, atau ditautkan manual oleh admin, semuanya muncul di sini — inilah pengganti “switch akun” untuk mobile: tampilkan semua anak dalam satu list/carousel, wali tinggal pilih kartu yang mana untuk dibuka detailnya.

200 OK

{
  "data": [
    {
      "id": 45,
      "nis": "1001000001",
      "nama": "Ahmad Fauzi",
      "jenis_kelamin": "L",
      "tempat_lahir": "Sumenep",
      "tanggal_lahir": "2012-03-10",
      "alamat": "...",
      "status": "aktif",
      "lembaga": "MTs Latee",
      "foto_url": null,
      "saldo": 200000,
      "hubungan": "wali"
    }
  ]
}

Detail satu anak

GET /api/wali/anak/{santri} butuh token

Response sama seperti satu item di atas.

Saldo

GET /api/wali/anak/{santri}/saldo butuh token

200 OK

{ "santri_id": 45, "saldo": 200000 }

Riwayat Transaksi

GET /api/wali/anak/{santri}/transaksi butuh token

Dipaginasi (20/halaman), memakai format standar Laravel paginator (data, links, meta). Gunakan ?page=2 dst.

200 OK

{
  "data": [
    {
      "id": 501,
      "uuid": "b7e1...",
      "jenis": "topup_transfer_wali",
      "arah": "kredit",
      "nominal": 50000,
      "saldo_sebelum": 150000,
      "saldo_sesudah": 200000,
      "status": "berhasil",
      "metode": "midtrans",
      "metode_detail": "bni_va",
      "biaya_midtrans": 4000,
      "biaya_ditanggung_wali": true,
      "catatan": null,
      "created_at": "2026-07-10T09:15:00+00:00",
      "tagihan": null,
      "referensi": null
    },
    {
      "id": 513,
      "uuid": "c4a9...",
      "jenis": "transfer_antar_santri",
      "arah": "debit",
      "nominal": 15000,
      "saldo_sebelum": 185000,
      "saldo_sesudah": 170000,
      "status": "berhasil",
      "metode": "sistem",
      "metode_detail": null,
      "biaya_midtrans": null,
      "biaya_ditanggung_wali": null,
      "catatan": null,
      "created_at": "2026-07-14T10:10:00+00:00",
      "tagihan": null,
      "referensi": { "type": "santri", "nama": "Muhammad Rizki", "nis": "1001000002" }
    }
  ],
  "links": { "first": "...", "last": "...", "prev": null, "next": null },
  "meta": { "current_page": 1, "last_page": 1, "per_page": 20, "total": 2 }
}

jenis salah satu dari: topup_tunai, topup_transfer_wali, penarikan_tunai, pembayaran_tagihan, penyesuaian, pembayaran_kantin, transfer_antar_santri. arah: debit atau kredit.

metode_detail adalah channel Midtrans spesifik (bni_va/bca_va/bri_va/qris) untuk baris topup_transfer_wali — tampilkan ini (bukan metode yang cuma "midtrans") kalau tersedia. biaya_midtrans & biaya_ditanggung_wali hanya terisi untuk topup_transfer_wali yang dibuat setelah fitur biaya Midtrans admin-configurable (/admin/pengaturan/midtrans) ada — null untuk jenis lain atau top up lama sebelum fitur ini. Kalau biaya_ditanggung_wali: true, wali sudah membayar nominal + biaya_midtrans lewat Midtrans meski nominal di sini tetap jumlah yang masuk saldo — tampilkan biayanya secara terpisah (lihat TransaksiDetailScreen di aplikasi mobile untuk contoh tampilan "Nominal Transfer / Biaya Admin / Total Transfer"). Kalau false atau 0, tidak perlu tampilkan apa-apa selain nominal (pondok yang menanggung).

referensi menunjukkan lawan transaksi ini — siapa yang menerima/mengirim, atau kantin mana yang dibayar. null kalau transaksi ini tidak punya lawan yang relevan ditampilkan (topup, bayar tagihan, penarikan tunai). Dua bentuk yang mungkin muncul:

referensi.typeMuncul untuk jenisField lain
santritransfer_antar_santrinama, nis — santri di sisi seberang transfer (kalau baris ini arah: debit, ini santri penerima; kalau kredit, ini santri pengirim)
unit_usahapembayaran_kantinnama, kode — kantin yang dibayar

Tagihan

List tagihan

GET /api/wali/anak/{santri}/tagihan butuh token

200 OK

{
  "data": [
    {
      "id": 88,
      "jenis_tagihan": { "kode": "SPP-BULANAN", "nama": "SPP Bulanan" },
      "periode_label": "2026-07",
      "nominal": 135000,
      "nominal_sebelum_diskon": 150000,
      "diskon_persen": 10,
      "nominal_terbayar": 0,
      "sisa": 135000,
      "status": "belum_lunas",
      "jatuh_tempo": "2026-07-21"
    }
  ]
}

status: belum_lunas, sebagian, lunas, dibatalkan. nominal_sebelum_diskon dan diskon_persen hanya terisi kalau santri punya kategori diskon yang berlaku untuk jenis tagihan tsb — kalau tidak ada diskon, keduanya null dan nominal adalah nominal penuh.

Bayar tagihan dari saldo

POST /api/wali/anak/{santri}/tagihan/{tagihan}/bayar butuh token

Melunasi tagihan memakai saldo santri yang sudah ada (bukan top up baru). Cocok saat saldo santri sudah cukup dan wali tidak ingin transfer lagi. Butuh PIN transaksi — lihat bagian PIN Transaksi di atas.

FieldTipeWajibKeterangan
nominalintegertidakNominal cicilan. Kosongkan (atau null) untuk melunasi penuh sisa tagihan sekaligus — nominal lebih kecil dari sisa hanya diterima kalau jenis tagihannya mendukung cicilan (bisa_dicicil).
pinstringyaPIN transaksi 6 digit.

200 OK

{
  "message": "Tagihan berhasil dibayar dari saldo.",
  "tagihan": { "id": 88, "...": "...", "status": "lunas" },
  "kwitansi_id": 91
}

kwitansi_id menunjuk kwitansi resmi yang baru diterbitkan otomatis untuk pembayaran ini (satu per pembayaran, bukan per tagihan — sebuah tagihan yang dicicil menghasilkan beberapa kwitansi terpisah). Ambil PDF-nya lewat Kwitansi Resmi di bawah.

422 — saldo tidak cukup, atau tagihan sudah lunas

{ "message": "Saldo santri tidak mencukupi untuk transaksi ini." }

422 — saldo cukup, tapi akan membuat saldo di bawah batas minimum

{ "message": "Saldo tidak bisa dipakai...", "code": "saldo_di_bawah_minimum" }

Beda dari "saldo tidak mencukupi" di atas (uangnya memang tidak cukup) — di sini saldo sebenarnya cukup, tapi kebijakan pondok (/admin/pengaturan/midtrans) menolak karena hasilnya akan membuat saldo santri turun di bawah batas minimum. Cek field code untuk membedakan keduanya di UI (mis. tampilkan tombol "Bayar Langsung via Midtrans" sebagai saran hanya untuk kasus ini).

Field pin yang kosong/salah format mengembalikan 422 validasi biasa; PIN yang salah (tapi 6 digit) mengembalikan 422 polos { "message": "PIN salah." } tanpa code; kelewat 5x salah mengembalikan 423 — lihat bagian PIN Transaksi di atas.

Bayar tagihan langsung via Midtrans (tanpa lewat saldo)

POST /api/wali/anak/{santri}/tagihan/{tagihan}/topup/core butuh token

Untuk saat saldo tidak cukup (atau wali tidak ingin memakainya) — membuat transaksi Midtrans Core API (VA/QRIS, sama seperti "Mulai top up dengan UI custom" di bawah) untuk persis sisa tagihan ini, bukan nominal bebas. Begitu dibayar, langsung melunasi tagihan tsb tanpa menyentuh saldo santri sama sekali (lihat TopupWaliService::createCoreApiTransactionForTagihan()) — padanan Core API dari tombol "Bayar Langsung via Midtrans" di portal web wali, yang di sana memakai Snap. Tidak ada varian Snap untuk endpoint ini: aplikasi mobile tidak punya WebView/browser-redirect, jadi hanya Core API (VA/QRIS, dirender native di app) yang tersedia lewat API ini.

FieldTipeWajibKeterangan
metodestringyaSalah satu dari bni_va, bca_va, bri_va, qris — tidak ada field nominal, server yang menentukan (persis sisa tagihan).

201 Created

Bentuk responsnya identik dengan respons "Mulai top up dengan UI custom" di bawah (field tagihan_id akan terisi, bukan null) — render dengan cara yang sama (_VaCard/_QrisCard di mobile), lalu poll status lewat GET /wali/topup/{topup} atau POST /wali/topup/{topup}/sync seperti biasa.

422 — tagihan sudah lunas, sudah ada pembayaran Midtrans yang masih pending untuk tagihan ini, atau Midtrans belum dikonfigurasi

{ "message": "Tagihan ini sudah lunas." }

404 — tagihan tidak tertaut ke santri ini

Modul Kantin

Pembayaran santri di kantin/unit usaha pondok, dipicu dengan memindai QR code yang menunjuk ke kode unit usaha (UnitUsaha.kode). Memotong saldo santri langsung (bukan tagihan) dan mengkredit saldo_unit milik kantin bersangkutan secara atomik (KantinPembayaranService) — sama seperti Bayar tagihan dari saldo, tunduk pada PIN transaksi dan batas minimum saldo (lihat bagian PIN Transaksi di atas dan Info pengaturan top up di bawah untuk angka batasnya), plus batas belanja kantin harian per santri kalau admin mengaktifkan kebijakannya (/admin/kantin/kebijakan, lihat kode limit_kantin_harian di bawah).

Cek info kantin dari kode QR

GET /api/wali/unit-usaha/{kode} butuh token

Dipanggil begitu QR berhasil dipindai, sebelum meminta wali memasukkan nominal — supaya aplikasi bisa menampilkan nama kantinnya ("Bayar ke Kantin Barokah") alih-alih hanya kode mentahnya. {kode} dicocokkan langsung ke unit_usahas.kode, bukan route-model-binding by id.

200 OK

{ "kode": "KANTIN-01", "nama": "Kantin Barokah" }

404 — kode tidak dikenal

{ "message": "Kantin tidak ditemukan." }

422 — kantin ditemukan tapi sedang tidak aktif

{ "message": "Kantin ini sedang tidak aktif." }

Bayar kantin dari saldo

POST /api/wali/anak/{santri}/bayar-kantin butuh token

FieldTipeWajibKeterangan
kodestringyaKode unit usaha, sama seperti dipakai di endpoint Cek info kantin di atas.
nominalintegeryaMinimal 1 (Rupiah, tanpa desimal).
pinstringyaPIN transaksi 6 digit.

200 OK

{
  "message": "Pembayaran ke Kantin Barokah berhasil.",
  "unit_usaha": { "kode": "KANTIN-01", "nama": "Kantin Barokah" },
  "id": 512,
  "santri": { "nama": "Ahmad Fauzi", "nis": "1001000001" },
  "nominal": 15000,
  "saldo_sesudah": 185000,
  "dibayar_at": "2026-07-14T10:05:00+00:00",
  "kwitansi_id": 87
}

kwitansi_id menunjuk baris kwitansis yang baru saja diterbitkan otomatis oleh KwitansiService untuk pembayaran ini — ambil PDF-nya lewat Kwitansi Resmi di bawah. id/dibayar_at tetap ada untuk kebutuhan tampilan yang tidak butuh dokumen resmi.

422 — saldo tidak cukup

{ "message": "Saldo santri tidak mencukupi untuk transaksi ini.", "code": "saldo_tidak_cukup" }

422 — saldo cukup, tapi akan membuat saldo di bawah batas minimum

{ "message": "Pembayaran tidak bisa dilakukan karena akan membuat saldo ... di bawah batas minimum Rp 100.000.", "code": "saldo_di_bawah_minimum" }

422 — melebihi batas belanja kantin harian (hanya jika kebijakannya aktif, lihat Skema Database — kebijakan_kantins)

{ "message": "Pembayaran ini melebihi batas belanja kantin harian ... (Rp 20.000). Sudah terpakai hari ini: Rp 15.000.", "code": "limit_kantin_harian" }

404 — kode kantin tidak ditemukan

Kwitansi Resmi

Kwitansi resmi bernomor permanen — berbeda dari struk informal (nomor diturunkan ulang dari id setiap kali diminta), sebuah kwitansi diterbitkan tepat sekali saat pembayaran tagihan atau kantin berhasil (KwitansiService, lihat kwitansi_id pada respons Bayar tagihan dari saldo dan Bayar kantin di atas), dan nomornya tidak pernah berubah walau diunduh berkali-kali.

Ambil tautan PDF kwitansi

GET /api/wali/kwitansi/{kwitansi} butuh token

Tidak langsung mengembalikan PDF-nya - aplikasi mobile tidak punya cara sederhana menempelkan token Bearer ke tab/aplikasi eksternal yang dibuka lewat url_launcher, jadi endpoint ini mengecek kepemilikan santri sekali di sini, lalu mengembalikan tautan bertanda tangan (URL::temporarySignedRoute) yang berlaku 15 menit. Aplikasi cukup membuka pdf_url apa adanya.

200 OK

{
  "nomor_kwitansi": "KWT-2026-000091",
  "pdf_url": "https://.../kwitansi/91/pdf?expires=...&signature=..."
}

403 — kwitansi milik santri yang tidak tertaut ke wali ini

Tautan pada pdf_url itu sendiri (GET /kwitansi/{kwitansi}/pdf, di luar prefix /api/wali) sengaja publik/tanpa token - signature-nya sendiri yang jadi otorisasi, sehingga bisa langsung dibuka di browser eksternal. Tautan yang kedaluwarsa atau signature yang tidak cocok (mis. URL diedit manual) mengembalikan 403.

Transfer Saldo Antar Santri (1 KK)

Memindahkan saldo langsung dari satu santri ke saudaranya yang terdaftar di Kartu Keluarga (No. KK) yang sama — dua baris ledger dibuat sekaligus dan atomik (debit di santri asal, kredit di santri tujuan, lihat TransferSaldoService). Tidak butuh persetujuan admin: uangnya tidak pernah keluar dari pondok, hanya berpindah kepemilikan antar santri. Sama seperti bayar kantin, tunduk pada PIN transaksi dan batas minimum saldo di sisi santri asal.

List saudara satu KK (calon tujuan transfer)

GET /api/wali/anak/{santri}/saudara butuh token

Hanya santri berstatus aktif dalam Kartu Keluarga yang sama dengan {santri}, tidak termasuk {santri} itu sendiri. Sengaja tidak dibatasi ke anak asuh wali yang sedang login saja — satu keluarga bisa punya lebih dari satu akun wali (lihat WaliAccountService), dan batas transfer yang disepakati adalah "1 KK", bukan "1 akun wali".

200 OK

{
  "data": [
    {
      "id": 46,
      "nis": "1001000002",
      "nama": "Muhammad Rizki",
      "jenis_kelamin": "L",
      "tempat_lahir": "Sumenep",
      "tanggal_lahir": "2014-05-02",
      "alamat": "...",
      "status": "aktif",
      "lembaga": "MTs Latee",
      "foto_url": null,
      "saldo": 0,
      "hubungan": null
    }
  ]
}

Bentuk responsnya sama seperti List semua anak yang tertaut di atas, dengan dua bedanya: field saldo selalu 0 di sini (endpoint ini tidak memuat data saldo — jangan ditampilkan sebagai saldo asli, cukup dipakai untuk daftar pilihan nama/NIS), dan hubungan biasanya null (santri ini bukan anak asuh wali yang sedang login, jadi tidak ada baris pivot wali_santris untuknya).

Transfer

POST /api/wali/anak/{santri}/transfer butuh token

{santri} di path adalah santri asal (saldo berkurang).

FieldTipeWajibKeterangan
ke_santri_idintegeryaID santri tujuan (harus ada di tabel santris) — ambil dari endpoint List saudara satu KK di atas.
nominalintegeryaMinimal 1 (Rupiah, tanpa desimal).
pinstringyaPIN transaksi 6 digit.

200 OK

{
  "message": "Transfer ke Muhammad Rizki berhasil.",
  "id": 513,
  "dari": { "id": 45, "nama": "Ahmad Fauzi", "saldo_sesudah": 170000 },
  "ke": { "id": 46, "nama": "Muhammad Rizki", "saldo_sesudah": 15000 },
  "nominal": 15000,
  "dibuat_at": "2026-07-14T10:10:00+00:00"
}

422 — saldo tidak cukup

{ "message": "Saldo santri tidak mencukupi untuk transaksi ini.", "code": "saldo_tidak_cukup" }

422 — saldo cukup, tapi akan membuat saldo di bawah batas minimum

{ "message": "Transfer tidak bisa dilakukan karena akan membuat saldo ... di bawah batas minimum Rp 100.000.", "code": "saldo_di_bawah_minimum" }

422 — ke_santri_id sama dengan {santri} sendiri, beda KK, atau santri tujuan sedang tidak aktif

{ "message": "Santri tujuan harus satu Kartu Keluarga." }

Ketiga kasus di atas tidak punya field code (beda dari saldo_tidak_cukup/saldo_di_bawah_minimum) — cukup tampilkan message-nya apa adanya, ini semua kesalahan input yang seharusnya sudah dicegah UI (mis. hanya menawarkan santri dari hasil List saudara satu KK).

Top Up Saldo (Midtrans)

Alur top up bisa lewat Midtrans Snap atau Core API (lihat di bawah). Nominal top up selalu masuk 100% ke saldo santri — tidak ada pemotongan otomatis untuk tagihan apapun, berapapun tagihan tertunggak yang santri punya. Untuk membayar tagihan, pakai salah satu dari dua endpoint di atas (dari saldo, atau langsung via Midtrans).

Sejak fitur biaya Midtrans admin-configurable ada (/admin/pengaturan/midtrans), Midtrans juga bisa memotong biaya transaksi — tapi biaya itu tidak pernah mengurangi nominal_diminta/saldo yang diterima santri. Kalau kebijakannya "bebankan ke wali", biayanya ditambahkan di atas nominal saat charge ke Midtrans (lihat biaya_midtrans pada respons di bawah); kalau "ditanggung pondok", wali cukup bayar nominal_diminta apa adanya. Endpoint Core API di bawah (yang dipakai UI custom) sudah menghitung ini otomatis — endpoint Snap tidak, karena channel pembayaran baru diketahui setelah Midtrans mengirim notifikasi, jadi biaya untuk top up via Snap selalu tercatat 0/ditanggung pondok.

Mulai top up

POST /api/wali/anak/{santri}/topup butuh token

FieldTipeWajibKeterangan
nominalintegeryaMinimal 10.000 (Rupiah, tanpa desimal)

201 Created

{
  "id": 77,
  "uuid": "f3d2...",
  "santri_id": 45,
  "tagihan_id": null,
  "nominal_diminta": 100000,
  "status": "pending",
  "nominal_potongan_tagihan": 0,
  "nominal_ke_saldo": 0,
  "biaya_midtrans": 0,
  "biaya_ditanggung_wali": false,
  "snap_token": "66e4fa55-....",
  "redirect_url": "https://app.sandbox.midtrans.com/snap/v4/redirection/66e4fa55-....",
  "payment_type": null,
  "va_bank": null,
  "va_number": null,
  "qr_url": null,
  "expiry_time": null,
  "paid_at": null,
  "created_at": "2026-07-11T10:00:00+00:00"
}

biaya_midtrans selalu 0 untuk jalur Snap ini (lihat catatan biaya di atas — channel pembayaran baru diketahui setelah pembayaran selesai, jadi tidak ada perhitungan biaya di sisi backend untuk jalur ini).

Dua cara memakai hasil ini di aplikasi mobile:

  • Midtrans Native SDK (Android/iOS): pakai snap_token langsung dengan Midtrans UI Kit SDK (MidtransSDK.getInstance().checkoutWithTransactionToken(...) di Android, atau MidtransUIKitSDK di iOS).
  • WebView sederhana: buka redirect_url di in-app browser/WebView. Setelah wali menyelesaikan pembayaran, tutup WebView dan lakukan polling status (lihat di bawah) — jangan asumsikan pembayaran sukses hanya dari WebView redirect, karena status final selalu ditentukan oleh notifikasi server-to-server dari Midtrans ke backend.

422 — Midtrans belum dikonfigurasi oleh admin pondok

{ "message": "Midtrans belum dikonfigurasi oleh admin pondok." }

Mulai top up dengan UI custom (Core API — VA BNI/BCA/BRI / QRIS)

POST /api/wali/anak/{santri}/topup/core butuh token

Alternatif dari endpoint Snap di atas, untuk aplikasi yang ingin membangun UI pembayaran sendiri (bukan redirect ke halaman Midtrans) memakai Midtrans Core API. Saat ini mendukung empat metode: Virtual Account BNI/BCA/BRI dan QRIS. Logika settle saldo persis sama seperti jalur Snap (selalu 100% ke saldo) — hanya cara memulai transaksinya yang beda.

FieldTipeWajibKeterangan
nominalintegeryaMinimal 10.000 (Rupiah, tanpa desimal)
metodestringyabni_va, bca_va, bri_va, atau qris

201 Created — metode: bni_va

{
  "id": 78,
  "uuid": "a1b2...",
  "santri_id": 45,
  "tagihan_id": null,
  "nominal_diminta": 100000,
  "status": "pending",
  "nominal_potongan_tagihan": 0,
  "nominal_ke_saldo": 0,
  "biaya_midtrans": 4000,
  "biaya_ditanggung_wali": true,
  "snap_token": null,
  "redirect_url": null,
  "payment_type": "bni_va",
  "va_bank": "bni",
  "va_number": "8808081234567890",
  "qr_url": null,
  "expiry_time": "2026-07-12T10:00:00+00:00",
  "paid_at": null,
  "created_at": "2026-07-11T10:00:00+00:00"
}

201 Created — metode: qris

{
  "id": 79,
  "...": "...",
  "biaya_midtrans": 700,
  "biaya_ditanggung_wali": true,
  "payment_type": "qris",
  "va_bank": null,
  "va_number": null,
  "qr_url": "https://api.sandbox.midtrans.com/v2/qris/a1b2.../qr-code",
  "expiry_time": "2026-07-11T10:15:00+00:00"
}

Untuk bni_va/bca_va/bri_va: tampilkan va_number (dan va_bank untuk label banknya) dengan tombol salin, minta wali transfer manual lewat m-banking/ATM bank yang sesuai ke Virtual Account tsb. Untuk qris: qr_url adalah URL gambar QR (PNG) siap ditampilkan langsung lewat Image.network(qr_url) atau setara — jangan generate QR sendiri dari string apapun, pakai URL ini apa adanya. Semua metode expired otomatis di sisi Midtrans pada expiry_time.

Nominal yang harus benar-benar ditransfer/dibayar wali adalah nominal_diminta + biaya_midtrans kalau biaya_ditanggung_wali: true (contoh di atas: wali transfer Rp 104.000 ke VA, bukan Rp 100.000 — VA/QRIS Midtrans sudah dibuat dengan gross_amount sejumlah itu), atau cukup nominal_diminta apa adanya kalau false. Jangan hardcode nominal_diminta saja sebagai jumlah yang ditransfer — selalu hitung totalnya dari kedua field ini. Nilai biaya_midtrans/biaya_ditanggung_wali di sini sudah final (dikunci saat charge dibuat) dan tidak berubah lagi meski admin mengubah pengaturan biaya setelahnya.

Setelah wali menyelesaikan pembayaran (transfer VA atau scan QRIS), tidak ada redirect/callback ke aplikasi — lakukan polling GET /topup/{topup} atau panggil POST /topup/{topup}/sync persis seperti alur Snap di bawah.

422 — metode tidak valid, atau Midtrans belum dikonfigurasi

{ "message": "The selected metode is invalid.", "errors": { "metode": ["The selected metode is invalid."] } }

Info pengaturan top up (untuk disclaimer di UI)

GET /api/wali/topup/pengaturan butuh token

Nama endpoint & field JSON minimal_saldo_setelah_topup dipertahankan apa adanya untuk kompatibilitas mundur dengan versi aplikasi mobile yang sudah dirilis, meski angkanya kini tidak lagi terkait top up: nilai ini adalah batas minimum saldo santri saat membayar tagihan dari saldo (lihat endpoint Bayar tagihan dari saldo di atas dan kode saldo_di_bawah_minimum pada respons 422-nya) — admin-editable lewat /admin/pengaturan/midtrans, jadi tidak boleh di-hardcode di aplikasi mobile.

200 OK

{
  "minimal_saldo_setelah_topup": 100000,
  "maksimal_nominal_transaksi": 50000000,
  "biaya_dibebankan_wali": true,
  "biaya_channel": {
    "bni_va": { "tipe": "tetap", "nilai": 4000 },
    "bca_va": { "tipe": "tetap", "nilai": 4000 },
    "bri_va": { "tipe": "tetap", "nilai": 4000 },
    "qris": { "tipe": "persen", "nilai": 0.7 }
  }
}

biaya_dibebankan_wali & biaya_channel adalah jadwal biaya Midtrans yang diatur admin di /admin/pengaturan/midtrans (default: false dan semua nilai: 0 sampai admin mengisinya). Ini adalah konfigurasi mentah, bukan nominal biaya yang sudah dihitung — hitung sendiri di sisi aplikasi sebelum submit, supaya estimasi biaya bisa berubah langsung saat wali mengetik nominal custom tanpa round-trip ke server tiap keystroke:

int hitungBiaya(String tipe, num nilai, int nominal) {
  return tipe == 'persen'
      ? (nominal * nilai / 100).round()
      : nilai.round();
}

Kalau biaya_dibebankan_wali: false, tidak perlu tampilkan estimasi apa pun di UI pra-submit — pondok yang menanggung, wali tetap bayar nominal_diminta apa adanya. Estimasi ini hanya untuk pratinjau sebelum submit; nilai final yang benar-benar dikunci ada di field biaya_midtrans/biaya_ditanggung_wali pada respons endpoint Core API di atas setelah transaksi benar-benar dibuat — pakai itu (bukan estimasi ini) untuk tampilan setelah top up dibuat.

Cek status top up (polling)

GET /api/wali/topup/{topup} butuh token

Backend menerima notifikasi Midtrans secara asynchronous (server-to-server webhook, bukan lewat aplikasi mobile). Setelah wali menutup halaman pembayaran, polling endpoint ini setiap beberapa detik sampai status bukan lagi pending.

200 OK

{
  "id": 77,
  "uuid": "f3d2...",
  "santri_id": 45,
  "tagihan_id": null,
  "nominal_diminta": 100000,
  "status": "paid",
  "nominal_potongan_tagihan": 0,
  "nominal_ke_saldo": 100000,
  "biaya_midtrans": 0,
  "biaya_ditanggung_wali": false,
  "snap_token": "66e4fa55-....",
  "redirect_url": "https://app.sandbox.midtrans.com/snap/v4/redirection/66e4fa55-....",
  "payment_type": null,
  "va_bank": null,
  "va_number": null,
  "qr_url": null,
  "expiry_time": null,
  "paid_at": "2026-07-11T10:02:15+00:00",
  "created_at": "2026-07-11T10:00:00+00:00"
}

status: pending, paid, expired, failed, cancelled, refunded. tagihan_id membedakan top up biasa (null) dari yang dibuat lewat endpoint Bayar tagihan langsung via Midtrans di atas (terisi) — berguna kalau layar polling perlu tahu apakah top up ini akan melunasi satu tagihan spesifik.

Saat status: "paid": untuk top up biasa nominal_potongan_tagihan selalu 0 dan nominal_ke_saldo selalu sama dengan nominal_diminta. Untuk top up yang di-scope ke tagihan (tagihan_id terisi), biasanya sebaliknya: nominal_potongan_tagihan sama dengan nominal_diminta dan nominal_ke_saldo nol — kecuali tagihannya keburu lunas lewat kanal lain sebelum pembayaran ini dikonfirmasi, baru sisanya masuk ke nominal_ke_saldo. Jumlah keduanya selalu sama dengan nominal_diminta.

Sinkronkan status manual dari Midtrans

POST /api/wali/topup/{topup}/sync butuh token

Notifikasi Midtrans (webhook) dikirim server-to-server ke backend, bukan lewat aplikasi mobile — jadi kalau backend belum sempat menerimanya (delay jaringan, atau saat development URL webhook belum publicly reachable), status GET /topup/{topup} bisa terlihat pending lebih lama dari seharusnya walau pembayaran sudah sukses di sisi Midtrans.

Endpoint ini mengambil status langsung dari Midtrans (bukan dari database lokal) dan menjalankan proses settle yang sama seperti webhook — aman dipanggil berkali-kali (idempoten). Gunakan sebagai tombol “Cek Status Sekarang” di UI kalau polling GET /topup/{topup} sudah beberapa saat tapi status belum berubah dari pending. Response sama seperti GET /topup/{topup}.

Pusat Notifikasi & Deep Link

Pusat notifikasi bersifat per akun wali, bukan per santri yang sedang dipilih. Karena satu wali dapat memiliki beberapa santri dalam satu KK, GET /api/wali/notifications menggabungkan notifikasi seluruh santri di bawah akun tersebut. Gunakan field santri_nama untuk menunjukkan pemilik aktivitas pada setiap item.

Setiap notifikasi baru disimpan ke tabel wali_notifications walaupun wali sedang offline atau belum memiliki token FCM. Push Firebase hanya menjadi kanal pengantar; daftar pada ikon lonceng tetap mengambil data persisten dari API.

  • Notifikasi transaksi membawa santri_id dan transaksi_id, lalu membuka detail melalui GET /api/wali/anak/{santri}/transaksi/{transaksi}.
  • Notifikasi tagihan membawa santri_id dan tagihan_id, lalu membuka detail melalui GET /api/wali/anak/{santri}/tagihan/{tagihan}.
  • Jenis lain yang belum memiliki halaman objek khusus membuka halaman Detail Notifikasi.
  • Backend selalu memastikan santri, transaksi, tagihan, dan notifikasi benar-benar dimiliki akun wali yang sedang login.

Respons transaksi menyertakan objek santri sebagai pemilik baris ledger. Detail transfer wajib memakai objek ini — bukan santri yang sedang aktif di UI — untuk menentukan pihak pengirim/penerima. Deep link push disimpan sebagai tujuan tertunda ketika aplikasi masih memulihkan sesi, berada di layar login, atau terkunci PIN/biometrik; tujuan baru dibuka setelah autentikasi selesai.

Mitigasi Transaksi pada Jaringan Lambat

Endpoint bayar tagihan, transfer antar santri, dan bayar kantin menerima request_id opsional maksimal 100 karakter. Aplikasi membuat satu nilai unik saat proses dimulai dan memakai nilai yang sama saat retry proses tersebut. Backend menyimpannya sebagai transaksis.idempotency_key; request ulang mengembalikan transaksi pertama tanpa mendebit saldo lagi.

  1. Kunci tombol dan tampilkan dialog proses yang tidak dapat ditutup selama request mutasi saldo berlangsung.
  2. Jangan menganggap timeout sebagai gagal karena respons dapat terlambat setelah transaksi berhasil dicatat server.
  3. Setelah timeout pembayaran tagihan, ambil detail tagihan yang sama. Untuk transfer atau kantin, cari transaksi terkait di riwayat terbaru.
  4. Jika perubahan ditemukan, tampilkan bahwa transaksi berhasil dikonfirmasi. Jika belum dapat dipastikan, minta pengguna memeriksa status/riwayat dan jangan langsung mengulang.
  5. Untuk top up Midtrans, polling status lalu gunakan endpoint sinkronisasi manual jika webhook terlambat.

Respons 401 berarti sesi API telah habis atau token tidak valid. Mobile menghapus token yang tidak valid dan mengarahkan ke login dengan penjelasan bahwa pengguna perlu masuk kembali, tetapi konfigurasi PIN/biometrik lokal tetap disimpan dan hanya dipakai kembali bila akun yang login sama. Timeout, maintenance, dan kegagalan jaringan saat restoreSession() tidak boleh menghapus token, PIN, atau preferensi sidik jari; pengguna dapat mencoba ulang lewat PIN/biometrik ketika koneksi pulih. Kegagalan jaringan juga tidak dihitung sebagai percobaan PIN salah. PIN login dan sidik jari diperlakukan sebagai satu paket login cepat: keduanya tetap aktif sampai dimatikan di profil, pengguna salah PIN lima kali, memilih Gunakan Password, atau berpindah akun. Penguncian akibat aplikasi tidak aktif berbeda dari sesi API habis: sesi tetap ada dan layar PIN/biometrik menampilkan alasan penguncian.

Ringkasan Endpoint

MethodPathKeterangan
GET/api/wali/app-infoBranding aplikasi (nama, logo) — publik, tanpa token
GET/api/wali/bannersBanner carousel Home yang aktif — publik, tanpa token
POST/api/wali/loginLogin (email atau No. KK), dapat token
POST/api/wali/logoutCabut token aktif
GET/api/wali/meProfil wali, termasuk must_change_password
PUT/api/wali/profileUbah nama/email/telepon wali
POST/api/wali/passwordUbah kata sandi
GET/api/wali/pin/statusCek apakah wali sudah punya PIN transaksi
POST/api/wali/pin/confirm-passwordVerifikasi kata sandi (langkah 1 pengaturan PIN)
POST/api/wali/pinAtur/ganti PIN transaksi (langkah 2)
GET/api/wali/anakList semua anak tertaut
GET/api/wali/anak/{santri}Detail satu anak
GET/api/wali/anak/{santri}/saldoSaldo anak
GET/api/wali/anak/{santri}/transaksiRiwayat transaksi (paginated)
GET/api/wali/anak/{santri}/transaksi/{transaksi}Detail transaksi untuk deep link notifikasi
GET/api/wali/anak/{santri}/tagihanList tagihan
GET/api/wali/anak/{santri}/tagihan/{tagihan}Detail tagihan untuk deep link notifikasi
POST/api/wali/anak/{santri}/tagihan/{tagihan}/bayarBayar tagihan dari saldo (butuh PIN)
POST/api/wali/anak/{santri}/tagihan/{tagihan}/topup/coreBayar tagihan langsung via Midtrans Core API
GET/api/wali/unit-usaha/{kode}Cek info kantin dari kode QR
POST/api/wali/anak/{santri}/bayar-kantinBayar kantin dari saldo (butuh PIN)
GET/api/wali/kwitansi/{kwitansi}Tautan PDF bertanda tangan untuk kwitansi resmi (15 menit)
GET/api/wali/anak/{santri}/saudaraList saudara satu KK (calon tujuan transfer)
POST/api/wali/anak/{santri}/transferTransfer saldo ke saudara satu KK (butuh PIN)
POST/api/wali/anak/{santri}/topupMulai top up via Midtrans Snap
POST/api/wali/anak/{santri}/topup/coreMulai top up via Core API (VA BNI/BCA/BRI / QRIS), untuk UI custom
GET/api/wali/topup/pengaturanInfo minimal saldo & jadwal biaya Midtrans untuk disclaimer top up
GET/api/wali/topup/{topup}Cek status top up
POST/api/wali/topup/{topup}/syncSinkronkan status manual langsung dari Midtrans
POST/api/wali/device-tokenDaftarkan/perbarui token FCM perangkat setelah login
DELETE/api/wali/device-tokenHapus token FCM perangkat saat logout
GET/api/wali/notificationsAmbil maksimal 100 notifikasi terbaru dan jumlah yang belum dibaca
POST/api/wali/notifications/{notification}/readTandai satu notifikasi milik wali sebagai dibaca
POST/api/wali/notifications/read-allTandai seluruh notifikasi wali sebagai dibaca

Catatan Versi & Batasan Saat Ini

  • Semua nominal uang dalam Rupiah bulat (integer, tanpa desimal).
  • Endpoint mutasi saldo (bayar tagihan, transfer antar santri, dan bayar kantin) menerima request_id opsional maksimal 100 karakter. Mobile mempertahankan nilai yang sama selama lima menit; retry dengan request_id yang sama mengembalikan transaksi pertama tanpa debit kedua. Saat respons timeout, aplikasi merekonsiliasi detail/riwayat terlebih dahulu dan memperingatkan pengguna agar tidak mengulang transaksi yang statusnya belum pasti.
  • Push notification Firebase dan pusat notifikasi persisten sudah aktif. Aplikasi mendaftarkan token lewat POST /api/wali/device-token dan menghapusnya lewat DELETE /api/wali/device-token. Setiap pesan juga disimpan di wali_notifications, sehingga tetap dapat dibaca dari ikon lonceng walaupun perangkat offline atau token FCM tidak ada. Backend mengirim notifikasi untuk tagihan baru, pengingat tiga hari sebelum jatuh tempo, top up berhasil, penarikan disetujui, serta debit saldo untuk pembayaran tagihan/kantin, transfer, dan penarikan tunai. Data sebelum migrasi pusat notifikasi tidak di-backfill.
  • Belum ada endpoint self-registration atau “lupa kata sandi” (reset tanpa tahu password lama) — POST /api/wali/password hanya untuk mengganti password yang sudah diketahui (termasuk kata sandi awal berupa No. KK). Pembuatan akun tetap hanya lewat admin di portal web; jika wali benar-benar lupa kata sandi, admin yang harus mengaturkannya ulang. Lupa PIN transaksi mengikuti pola yang sama — tidak ada endpoint self-service reset, hanya admin lewat /admin/users.
  • Kredensial Midtrans (server key / client key) diatur oleh admin lewat panel web (/admin/pengaturan/midtrans), bisa sandbox atau produksi. Jika POST /topup mengembalikan 422 “Midtrans belum dikonfigurasi”, hubungi admin pondok.
  • PIN transaksi, batas minimum saldo, dan pembayaran kantin/transfer antar santri semuanya baru ditambahkan pada rilis yang sama (lihat bagian PIN Transaksi, Modul Kantin, dan Transfer Saldo Antar Santri di atas) — versi aplikasi mobile yang lebih lama dari itu tidak mengirim field pin sama sekali dan akan selalu mendapat 422 validasi pada ketiga endpoint aksi tsb.
  • GET /api/wali/app-info (lihat Info Aplikasi di atas) juga baru — dipakai aplikasi mobile untuk menampilkan logo/nama aplikasi hasil unggahan admin di layar splash, login, "Tentang Aplikasi", serta kop kwitansi & e-statement yang dicetak dari aplikasi. Versi mobile yang lebih lama tidak memanggil endpoint ini sama sekali dan tetap menampilkan branding bawaan yang dibundel di dalam aplikasi — tidak ada endpoint ini bukan error, hanya belum diperbarui.