Cara Menghitung Denda Keterlambatan Sewa di PHP & MySQL (Studi Kasus Rental Mobil)
Hampir semua aplikasi sewa (rental mobil, rental kamera, perpustakaan, kost) punya satu aturan yang sama: kalau barang telat dikembalikan, penyewa kena denda. Kelihatannya sepele, tapi kalau salah hitung, angka di invoice bisa selisih dan bikin pelanggan komplain.
Di tutorial ini kita bikin logika denda keterlambatan yang rapi pakai PHP Native dan MySQL, lengkap dengan cara menyimpannya ke database supaya aman dari dobel sewa. Contohnya diambil dari aplikasi Sistem Rental Mobil yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Tentukan aturan dendanya
Sebelum ngoding, tulis dulu aturannya. Contoh aturan yang umum dipakai rental mobil:
- Mobil disewa dari tanggal mulai sampai tanggal selesai (jadwal kembali).
- Kalau dikembalikan setelah tanggal selesai, tiap hari telat didenda 120% dari harga sewa harian.
- Kalau dikembalikan tepat waktu atau lebih cepat, denda Rp 0 (tidak ada potongan harga).
Persentase denda sebaiknya tidak di-hardcode, tapi disimpan di tabel pengaturan supaya pemilik usaha bisa mengubahnya sendiri:
CREATE TABLE pengaturan (
id INT PRIMARY KEY,
nama_usaha VARCHAR(100) NOT NULL,
denda_per_hari_persen INT NOT NULL DEFAULT 120
);
INSERT INTO pengaturan (id, nama_usaha, denda_per_hari_persen)
VALUES (1, 'Rental Mobil Sejahtera', 120);
2. Struktur tabel transaksi sewa
Yang penting di tabel sewa: simpan harga per hari saat transaksi terjadi, jangan cuma relasi ke tabel mobil. Kalau besok harga mobil naik, invoice lama tidak ikut berubah.
CREATE TABLE sewa (
id INT AUTO_INCREMENT PRIMARY KEY,
kode VARCHAR(20) NOT NULL UNIQUE,
mobil_id INT NOT NULL,
tgl_mulai DATE NOT NULL,
tgl_selesai DATE NOT NULL, -- jadwal kembali
tgl_kembali DATE NULL, -- diisi saat mobil benar-benar kembali
lama_hari INT NOT NULL,
harga_per_hari INT NOT NULL, -- "snapshot" harga saat transaksi
subtotal INT NOT NULL,
denda INT NOT NULL DEFAULT 0,
total INT NOT NULL,
status ENUM('berjalan','selesai','batal') NOT NULL DEFAULT 'berjalan'
);
3. Fungsi menghitung denda
Kuncinya ada di menghitung selisih hari. Kolom DATE dari MySQL formatnya Y-m-d, jadi aman diubah ke timestamp lalu dibagi 86.400 (jumlah detik dalam sehari):
<?php
/**
* Hitung denda keterlambatan.
* Mengembalikan [jumlah_hari_telat, nominal_denda].
*/
function hitung_denda(string $tglSelesai, string $tglKembali, int $hargaPerHari, int $persenDenda): array
{
$selisihDetik = strtotime($tglKembali) - strtotime($tglSelesai);
$telat = (int) floor($selisihDetik / 86400);
// Kembali lebih cepat atau tepat waktu = tidak telat
$telat = max(0, $telat);
$denda = (int) round($telat * $hargaPerHari * $persenDenda / 100);
return [$telat, $denda];
}
Coba tes dengan beberapa kasus:
// Jadwal kembali 18 Okt, harga Rp 400.000/hari, denda 120%
print_r(hitung_denda('2026-10-18', '2026-10-18', 400000, 120)); // [0, 0] tepat waktu
print_r(hitung_denda('2026-10-18', '2026-10-17', 400000, 120)); // [0, 0] lebih cepat
print_r(hitung_denda('2026-10-18', '2026-10-19', 400000, 120)); // [1, 480000] telat 1 hari
print_r(hitung_denda('2026-10-18', '2026-10-21', 400000, 120)); // [3, 1440000] telat 3 hari
Kenapa pakai
max(0, ...)? Tanpa itu, mobil yang kembali lebih cepat menghasilkan hari telat negatif dan denda jadi minus, yang artinya sistem diam-diam memberi diskon.
4. Simpan pengembalian dengan transaksi database
Saat pengembalian, ada dua tabel yang berubah sekaligus: transaksi sewa jadi selesai, dan status mobil kembali tersedia. Kalau salah satunya gagal, data jadi tidak konsisten (misalnya sewa selesai tapi mobil masih tercatat "disewa"). Karena itu bungkus dengan beginTransaction():
<?php
$pdo = new PDO('mysql:host=localhost;dbname=rental_mobil;charset=utf8mb4', 'root', '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$sewaId = (int) $_POST['id'];
$tglKembali = $_POST['tgl_kembali']; // contoh: 2026-10-19
$persen = (int) $pdo->query('SELECT denda_per_hari_persen FROM pengaturan WHERE id = 1')->fetchColumn();
$st = $pdo->prepare("SELECT * FROM sewa WHERE id = ? AND status = 'berjalan'");
$st->execute([$sewaId]);
$sewa = $st->fetch();
if (!$sewa) {
die('Transaksi tidak ditemukan atau sudah selesai.');
}
[$telat, $denda] = hitung_denda($sewa['tgl_selesai'], $tglKembali, (int) $sewa['harga_per_hari'], );
= [] + ;
{
->();
->()
->([, , , ]);
->()
->([[]]);
->();
. (, , , );
} ( ) {
->();
. ->();
}
5. Bonus: cegah satu mobil tersewa dua kali
Masalah klasik aplikasi rental: dua petugas membuka form di waktu bersamaan dan sama-sama memilih mobil yang sama. Solusinya, kunci baris mobil dengan SELECT ... FOR UPDATE di dalam transaksi, lalu cek ulang statusnya sebelum menyimpan:
$pdo->beginTransaction();
$st = $pdo->prepare('SELECT * FROM mobil WHERE id = ? FOR UPDATE');
$st->execute([$mobilId]);
$mobil = $st->fetch();
if ($mobil['status'] !== 'tersedia') {
$pdo->rollBack();
die('Mobil sudah tidak tersedia.');
}
// ... INSERT INTO sewa ...
$pdo->prepare("UPDATE mobil SET status = 'disewa' WHERE id = ?")->execute([$mobilId]);
$pdo->commit();
Petugas kedua akan "menunggu" sampai transaksi petugas pertama selesai, lalu mendapat pesan bahwa mobil sudah tidak tersedia.
Ringkasan
- Simpan harga per hari di tabel transaksi supaya invoice lama tidak berubah.
- Hitung hari telat dari selisih tanggal, lalu pakai
max(0, ...)agar tidak pernah negatif. - Simpan persentase denda di tabel pengaturan, bukan di kode.
- Bungkus update yang menyentuh lebih dari satu tabel dengan database transaction.
- Pakai
SELECT ... FOR UPDATEuntuk mencegah satu unit disewa dua kali.
Download source code lengkapnya
Semua logika di atas sudah dipakai di aplikasi Sistem Rental Mobil PHP Native & MySQL: transaksi sewa dengan sopir, DP dan pelunasan, invoice siap cetak, grafik pendapatan, dan laporan yang bisa diekspor ke Excel. Gratis dan bisa langsung jalan di XAMPP.
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.