Cara Menghitung Tunggakan SPP per Bulan di PHP & MySQL (Plus Anti Bayar Dobel)
Aplikasi SPP terlihat sederhana: siswa bayar, dicatat, selesai. Masalah baru muncul ketika bendahara bertanya, "Siapa saja yang menunggak, berapa bulan, dan berapa totalnya?" Kalau struktur datanya salah, pertanyaan ini sulit dijawab dan rawan salah hitung.
Di tutorial ini kita bahas cara menyimpan pembayaran SPP per bulan yang benar, menghitung tunggakan otomatis berdasarkan tanggal jatuh tempo, dan mencegah satu bulan terbayar dua kali. Contohnya diambil dari aplikasi Sistem Pembayaran SPP Sekolah yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Simpan satu baris per bulan, bukan per transaksi
Kesalahan yang sering terjadi: menyimpan pembayaran sebagai teks, misalnya kolom bulan = "Juli, Agustus, September". Data seperti ini tidak bisa dihitung dengan SQL.
Pisahkan menjadi dua tabel:
-- Satu transaksi = satu kwitansi (bisa beberapa bulan)
CREATE TABLE transaksi (
id INT AUTO_INCREMENT PRIMARY KEY,
kode VARCHAR(20) NOT NULL UNIQUE,
siswa_id INT NOT NULL,
tahun_ajaran_id INT NOT NULL,
total INT NOT NULL,
tgl_bayar DATE NOT NULL
) ENGINE=InnoDB;
-- Satu baris per bulan yang dibayar
CREATE TABLE pembayaran (
id INT AUTO_INCREMENT PRIMARY KEY,
transaksi_id INT NOT NULL,
siswa_id INT NOT NULL,
tahun_ajaran_id INT NOT NULL,
bulan TINYINT NOT NULL, -- 1..12
nominal INT NOT NULL,
UNIQUE KEY uk_bulan (siswa_id, tahun_ajaran_id, bulan),
FOREIGN KEY (transaksi_id) REFERENCES transaksi(id) ON DELETE CASCADE
) ENGINE=InnoDB;
Dua hal penting di sini:
UNIQUE (siswa_id, tahun_ajaran_id, bulan): database sendiri yang menolak bulan yang sama dibayar dua kali. Ini lebih kuat daripada pengecekan di PHP, karena tetap aman walaupun dua petugas menyimpan di detik yang sama.ON DELETE CASCADE: kalau transaksi dibatalkan, baris bulannya ikut terhapus, sehingga bulan-bulan itu otomatis kembali berstatus belum lunas.
2. Urutan bulan tahun ajaran
Tahun ajaran di Indonesia dimulai Juli dan berakhir Juni, jadi tahun ajaran 2026/2027 berarti Juli 2026 sampai Juni 2027. Simpan urutannya sebagai konstanta:
<?php
const BULAN_AJARAN = [7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6];
Untuk mengetahui tahun kalender sebuah bulan: bulan 7 sampai 12 memakai tahun mulai, sedangkan bulan 1 sampai 6 memakai tahun mulai + 1.
3. Bulan mana saja yang sudah jatuh tempo?
Misalnya SPP jatuh tempo setiap tanggal 10. Sebuah bulan dianggap jatuh tempo jika tanggal 10 bulan itu sudah lewat hari ini:
<?php
function bulan_jatuh_tempo(int $tahunMulai, int $tanggalJatuhTempo): array
{
$hasil = [];
foreach (BULAN_AJARAN as $b) {
$tahun = $b >= 7 ? $tahunMulai : $tahunMulai + 1;
$jatuhTempo = sprintf('%04d-%02d-%02d', $tahun, $b, $tanggalJatuhTempo);
if ($jatuhTempo <= date('Y-m-d')) {
$hasil[] = $b;
}
}
return $hasil;
}
// Jika hari ini 19 Oktober 2026 dan tahun ajaran 2026/2027:
print_r(bulan_jatuh_tempo(2026, 10)); // [7, 8, 9, 10]
Membandingkan string tanggal format Y-m-d dengan <= aman, karena urutan alfabetnya sama dengan urutan waktunya.
4. Hitung tunggakan satu siswa
Tunggakan adalah bulan yang sudah jatuh tempo tetapi belum ada di tabel pembayaran:
<?php
function tunggakan_siswa(PDO $pdo, int $siswaId, int $taId, int $tahunMulai, int $nominal): array
{
$st = $pdo->prepare('SELECT bulan FROM pembayaran WHERE siswa_id = ? AND tahun_ajaran_id = ?');
$st->execute([$siswaId, $taId]);
$sudahBayar = array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN));
$menunggak = array_values(array_diff(bulan_jatuh_tempo($tahunMulai, 10), $sudahBayar));
return [
'bulan' => $menunggak, // contoh: [9, 10]
'total' => count($menunggak) * $nominal, // contoh: 2 x 175.000 = 350.000
];
}
Kalau siswa punya potongan beasiswa, hitung nominalnya lebih dulu: $nominal = round($tarif * (100 - $potonganPersen) / 100). Jika hasilnya 0 (beasiswa 100%), siswa itu tidak pernah dianggap menunggak.
5. Simpan pembayaran beberapa bulan sekaligus
Bungkus insert transaksi dan detail bulannya dalam satu transaksi database. Jika salah satu bulan ternyata sudah lunas, constraint UNIQUE akan melempar error dan semua perubahan dibatalkan, sehingga tidak ada kwitansi setengah jadi.
<?php
try {
$pdo->beginTransaction();
$pdo->prepare('INSERT INTO transaksi (kode, siswa_id, tahun_ajaran_id, total, tgl_bayar) VALUES (?,?,?,?,CURDATE())')
->execute([$kode, $siswaId, $taId, $nominal * count($bulan)]);
$transaksiId = (int) $pdo->lastInsertId();
$ins = $pdo->prepare('INSERT INTO pembayaran (transaksi_id, siswa_id, tahun_ajaran_id, bulan, nominal) VALUES (?,?,?,?,?)');
foreach ($bulan as $b) {
$ins->execute([$transaksiId, $siswaId, $taId, $b, $nominal]);
}
$pdo->commit();
} catch (PDOException $e) {
$pdo->rollBack();
if ($e->errorInfo[1] === 1062) { // 1062 = duplicate entry
echo 'Sebagian bulan yang dipilih sudah lunas.';
} else {
;
}
}
6. Bonus: pengingat WhatsApp ke orang tua
Setelah daftar bulan menunggak didapat, buat link wa.me dengan pesan yang sudah terisi:
<?php
$nomor = preg_replace('/^0/', '62', preg_replace('/\D/', '', $telpWali)); // 0812... -> 62812...
$pesan = "Assalamualaikum, SPP ananda $nama bulan September, Oktober belum dibayarkan, total Rp 350.000.";
echo '<a href="https://wa.me/' . $nomor . '?text=' . rawurlencode($pesan) . '" target="_blank">Ingatkan via WA</a>';
Ringkasan
- Simpan satu baris per bulan yang dibayar, dikelompokkan oleh tabel transaksi (kwitansi).
- Pasang UNIQUE (siswa, tahun ajaran, bulan) supaya tidak ada bayar dobel.
- Tentukan bulan jatuh tempo dari urutan tahun ajaran Juli sampai Juni dan tanggal jatuh tempo.
- Tunggakan = bulan jatuh tempo dikurangi bulan yang sudah dibayar.
- Simpan pembayaran multi-bulan di dalam transaksi database.
Download source code lengkapnya
Semua logika di atas sudah dipakai di Sistem Pembayaran SPP Sekolah PHP Native & MySQL: kartu SPP 12 bulan, pembayaran multi-bulan dengan kwitansi, potongan beasiswa, daftar tunggakan dengan pengingat WhatsApp, laporan penerimaan, dan halaman cek tagihan untuk orang tua. Gratis dan langsung jalan di XAMPP.
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.