MIF_E31230069/spk_kontrakan/app/Models/Booking.php

426 lines
11 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class Booking extends Model
{
use HasFactory;
/**
* Boot method - Auto sync kontrakan status saat booking di-update atau dihapus
*/
protected static function boot()
{
parent::boot();
// Saat booking dihapus, otomatis update status kontrakan
static::deleted(function ($booking) {
static::syncKontrakanStatus($booking->kontrakan_id);
});
}
/**
* Sinkronisasi status kontrakan berdasarkan booking aktif
* Dipanggil otomatis setelah booking dihapus/cancel/checkout
*/
public static function syncKontrakanStatus($kontrakanId)
{
// Cek apakah ada booking checked_in (sewa)
$hasCheckedIn = static::where('kontrakan_id', $kontrakanId)
->where('status', self::STATUS_CHECKED_IN)
->where('jenis_pengajuan', 'sewa')
->exists();
if ($hasCheckedIn) {
Kontrakan::where('id', $kontrakanId)->update(['status' => 'occupied']);
return;
}
// Cek apakah ada booking confirmed atau pending (sewa)
$hasActiveOrPending = static::where('kontrakan_id', $kontrakanId)
->whereIn('status', [self::STATUS_CONFIRMED, self::STATUS_PENDING])
->where('jenis_pengajuan', 'sewa')
->exists();
if ($hasActiveOrPending) {
Kontrakan::where('id', $kontrakanId)->update(['status' => 'booked']);
return;
}
// Tidak ada booking aktif → kontrakan tersedia
Kontrakan::where('id', $kontrakanId)->update([
'status' => 'available',
'occupied_until' => null,
]);
}
protected $fillable = [
'kontrakan_id',
'user_id',
'start_date',
'end_date',
'status',
'amount',
'payment_status',
'payment_method',
'payment_proof',
'ktp_photo',
'jenis_pengajuan',
'paid_at',
'notes',
'booking_source',
'tenant_name',
'tenant_phone',
'confirmed_at',
'checked_in_at',
'checked_out_at',
'cancelled_at',
'cancellation_reason',
'payment_rejection_reason',
'jam_survei',
'tanggal_survei',
];
protected $casts = [
'start_date' => 'date',
'end_date' => 'date',
'amount' => 'decimal:2',
'paid_at' => 'datetime',
'confirmed_at' => 'datetime',
'checked_in_at' => 'datetime',
'checked_out_at' => 'datetime',
'cancelled_at' => 'datetime',
];
// ========== STATUS CONSTANTS ==========
const STATUS_PENDING = 'pending';
const STATUS_CONFIRMED = 'confirmed';
const STATUS_CHECKED_IN = 'checked_in';
const STATUS_COMPLETED = 'completed';
const STATUS_CANCELLED = 'cancelled';
const PAYMENT_UNPAID = 'unpaid';
const PAYMENT_PAID = 'paid';
const PAYMENT_VERIFICATION = 'verification';
const PAYMENT_REFUNDED = 'refunded';
// ========== RELASI ==========
/**
* Booking dimiliki oleh satu Kontrakan
*/
public function kontrakan()
{
return $this->belongsTo(Kontrakan::class);
}
/**
* Booking dimiliki oleh satu User (penyewa)
*/
public function user()
{
return $this->belongsTo(User::class);
}
// ========== SCOPES ==========
/**
* Scope: Booking yang aktif (confirmed atau checked_in)
*/
public function scopeActive(Builder $query): Builder
{
return $query->whereIn('status', [self::STATUS_CONFIRMED, self::STATUS_CHECKED_IN]);
}
/**
* Scope: Booking yang pending
*/
public function scopePending(Builder $query): Builder
{
return $query->where('status', self::STATUS_PENDING);
}
/**
* Scope: Booking yang sudah dibatalkan
*/
public function scopeCancelled(Builder $query): Builder
{
return $query->where('status', self::STATUS_CANCELLED);
}
/**
* Scope: Booking yang overlap dengan periode tertentu
* Digunakan untuk cek konflik
*/
public function scopeOverlapping(Builder $query, $startDate, $endDate, $excludeId = null): Builder
{
$query->where(function ($q) use ($startDate, $endDate) {
// Cek overlap: start1 <= end2 AND end1 >= start2
$q->where('start_date', '<=', $endDate)
->where('end_date', '>=', $startDate);
});
if ($excludeId) {
$query->where('id', '!=', $excludeId);
}
return $query;
}
/**
* Scope: Booking untuk kontrakan tertentu
*/
public function scopeForKontrakan(Builder $query, $kontrakanId): Builder
{
return $query->where('kontrakan_id', $kontrakanId);
}
/**
* Scope: Booking yang mencakup hari ini
*/
public function scopeCurrentlyActive(Builder $query): Builder
{
$today = now()->toDateString();
return $query->active()
->where('start_date', '<=', $today)
->where('end_date', '>=', $today);
}
/**
* Scope: Booking yang siap untuk auto-verify
* (pembayaran sudah, belum checked_in, check-in date sudah tiba)
*/
public function scopeReadyForAutoVerify(Builder $query): Builder
{
$today = now()->toDateString();
return $query->where('payment_status', self::PAYMENT_PAID)
->whereIn('status', [self::STATUS_PENDING, self::STATUS_CONFIRMED])
->where('start_date', '<=', $today)
->whereNull('checked_in_at');
}
// ========== HELPERS ==========
/**
* Cek apakah booking ini aktif (confirmed atau checked_in)
*/
public function isActive(): bool
{
return in_array($this->status, [self::STATUS_CONFIRMED, self::STATUS_CHECKED_IN]);
}
/**
* Cek apakah booking mencakup hari ini
*/
public function isCurrentlyOccupying(): bool
{
if (!$this->isActive()) {
return false;
}
$today = now()->toDateString();
return $this->start_date->lte($today) && $this->end_date->gte($today);
}
/**
* Cek apakah booking bisa dibatalkan
*/
public function canBeCancelled(): bool
{
return in_array($this->status, [self::STATUS_PENDING, self::STATUS_CONFIRMED]);
}
/**
* Hitung durasi sewa dalam hari
*/
public function getDurationDaysAttribute(): int
{
return $this->start_date->diffInDays($this->end_date) + 1;
}
/**
* Hitung durasi sewa dalam bulan (pembulatan)
*/
public function getDurationMonthsAttribute(): float
{
return round($this->start_date->diffInDays($this->end_date) / 30, 1);
}
/**
* Get status label untuk UI
*/
public function getStatusLabelAttribute(): string
{
return match ($this->status) {
self::STATUS_PENDING => 'Menunggu Konfirmasi',
self::STATUS_CONFIRMED => 'Dikonfirmasi',
self::STATUS_CHECKED_IN => 'Sedang Ditempati',
self::STATUS_COMPLETED => 'Selesai',
self::STATUS_CANCELLED => 'Dibatalkan',
default => ucfirst($this->status),
};
}
/**
* Get status badge class untuk UI
*/
public function getStatusBadgeClassAttribute(): string
{
return match ($this->status) {
self::STATUS_PENDING => 'bg-warning text-dark',
self::STATUS_CONFIRMED => 'bg-info',
self::STATUS_CHECKED_IN => 'bg-success',
self::STATUS_COMPLETED => 'bg-secondary',
self::STATUS_CANCELLED => 'bg-danger',
default => 'bg-secondary',
};
}
/**
* Get payment status label
*/
public function getPaymentStatusLabelAttribute(): string
{
return match ($this->payment_status) {
self::PAYMENT_UNPAID => 'Belum Bayar',
self::PAYMENT_VERIFICATION => 'Menunggu Verifikasi',
self::PAYMENT_PAID => 'Lunas',
self::PAYMENT_REFUNDED => 'Dikembalikan',
default => ucfirst($this->payment_status),
};
}
/**
* Cek apakah booking ini adalah pengajuan survei
*/
public function isSurvey(): bool
{
return $this->jenis_pengajuan === 'survei';
}
/**
* Cek apakah booking ini adalah pengajuan sewa
* Data lama yang belum memiliki jenis tetap dianggap sebagai sewa.
*/
public function isSewa(): bool
{
return !$this->isSurvey();
}
// ========== ACTIONS ==========
/**
* Konfirmasi booking
*/
public function confirm(): bool
{
if ($this->status !== self::STATUS_PENDING) {
return false;
}
$this->status = self::STATUS_CONFIRMED;
$this->confirmed_at = now();
$this->save();
// Sync status kontrakan otomatis
static::syncKontrakanStatus($this->kontrakan_id);
return true;
}
/**
* Check-in (penyewa masuk)
*/
public function checkIn(): bool
{
if ($this->status !== self::STATUS_CONFIRMED) {
return false;
}
$this->status = self::STATUS_CHECKED_IN;
$this->checked_in_at = now();
$this->save();
// Sync status kontrakan otomatis + set occupied_until
$this->kontrakan->update(['occupied_until' => $this->end_date]);
static::syncKontrakanStatus($this->kontrakan_id);
return true;
}
/**
* Check-out (penyewa keluar)
*/
public function checkOut(): bool
{
if ($this->status !== self::STATUS_CHECKED_IN) {
return false;
}
$this->status = self::STATUS_COMPLETED;
$this->checked_out_at = now();
$this->save();
// Sync status kontrakan otomatis
static::syncKontrakanStatus($this->kontrakan_id);
return true;
}
/**
* Batalkan booking
*/
public function cancel(?string $reason = null): bool
{
if (!$this->canBeCancelled()) {
return false;
}
$this->status = self::STATUS_CANCELLED;
$this->cancelled_at = now();
$this->cancellation_reason = $reason;
$this->save();
// Sync status kontrakan otomatis
static::syncKontrakanStatus($this->kontrakan_id);
return true;
}
/**
* Tandai sebagai lunas
*/
public function markAsPaid(?string $method = null): bool
{
$this->payment_status = self::PAYMENT_PAID;
$this->paid_at = now();
$this->payment_method = $method;
return $this->save();
}
// ========== STATIC HELPERS ==========
/**
* Cek apakah ada konflik booking untuk kontrakan & periode tertentu
*/
public static function hasConflict($kontrakanId, $startDate, $endDate, $excludeId = null): bool
{
return static::forKontrakan($kontrakanId)
->active()
->overlapping($startDate, $endDate, $excludeId)
->exists();
}
/**
* Cek ketersediaan kontrakan untuk periode tertentu
*/
public static function isAvailable($kontrakanId, $startDate, $endDate, $excludeId = null): bool
{
return !static::hasConflict($kontrakanId, $startDate, $endDate, $excludeId);
}
}