# Reservation Module — Overview

## Tujuan Module

Module Reservation mengelola pemesanan tempat/event oleh customer (banquet/private event) di restoran. Cakupannya meliputi:

- Pendaftaran reservasi dari form publik maupun via aplikasi POS/kasir
- Penugasan ruang dan meja secara spesifik (TableGroup → TableMap)
- Validasi bentrok jadwal sebelum data disimpan
- Konfigurasi slot waktu, harga, dan kapasitas per cabang
- Pemblokiran tanggal tertentu (DisableDate)
- Penagihan via Invoice AR (Account Receivable)
- Pembayaran invoice termasuk penggunaan saldo deposit customer
- Integrasi pembayaran online via **Midtrans**
- Notifikasi konfirmasi via **email**
- Pencatatan order meja (Penjualan) yang terhubung ke reservasi

---

## Flow Bisnis

```
[Customer / Staf]
      │
      ▼
1. BOOKING RESERVASI
   ├── Public form (index) → pilih tanggal, jam, jumlah tamu, event
   └── Backend / POS app  → input reservasi langsung oleh staf
          │
          │  Validasi slot:
          │  ├── Cek ReservasiPrice (kapasitas maks per slot waktu)
          │  ├── Cek DisableDate (tanggal/jam diblokir)
          │  └── Cek CONFLICT: table_group + table_map + date + time
          │       → jika sudah dipakai reservasi lain → DITOLAK
          │
          ▼
2. RESERVASI TERSIMPAN (reservasi_biodata)
   Status invoice : Uninvoiced (1)
   No Reservasi   : prefix EV-{cabang}-{bulan} (backend)
                    atau RSV-{cabang}-{bulan} (POS app)
          │
          ├──► [Opsional] Pembayaran online Midtrans (public booking)
          │         actionBayar → Snap Token → midtrans_status: Paid/Pending
          │         actionSuccess → kirim email konfirmasi
          │
          ├──► [Kasir POS] Buat Order (Penjualan)
          │         Penjualan.reservation_id → ReservasiBiodata.id
          │
          ▼
3. INVOICE AR DIBUAT (reservasi_invoice)
   Staf pilih customer dan reservasi yang belum diinvoice
   Satu invoice bisa mencakup banyak reservasi (via ReservasiInvoiceDetail)
   Status invoice reservasi: Uninvoiced → Partial Invoiced → Invoiced
          │
          ▼
4. PEMBAYARAN INVOICE (reservasi_payment)
   Staf input pembayaran terhadap invoice yang masih unpaid/partial
   ├── Bayar tunai/transfer (amount langsung)
   └── Gunakan deposit customer (deposit_used → kurangi saldo deposit)
   Status invoice: unpaid → partial → paid
          │
          ▼
5. SELESAI
   Invoice status              : paid
   ReservasiBiodata.status_invoice : Invoiced (3)
```

---

## Entity yang Digunakan

### Tabel Inti

| Model | Tabel | Keterangan |
|---|---|---|
| `ReservasiBiodata` | `reservasi_biodata` | Data utama reservasi per-customer |
| `ReservasiInvoice` | `reservasi_invoice` | Invoice AR yang ditagihkan ke customer |
| `ReservasiInvoiceDetail` | `reservasi_invoice_detail` | Mapping invoice ↔ reservasi (M:M) |
| `ReservasiPayment` | `reservasi_payment` | Header pembayaran invoice |
| `ReservasiPaymentDetail` | `reservasi_payment_detail` | Detail pembayaran per invoice |
| `ReservasiPrice` | `reservasi_price` | Konfigurasi harga & kapasitas per slot waktu |
| `DisableDate` | `disable_date` | Tanggal/jam yang diblokir dari booking |

### Tabel Terkait (Dependensi)

| Model | Keterangan |
|---|---|
| `Customer` | Pemilik reservasi; punya field `deposit` (saldo deposit) |
| `TableGroup` | Pengelompokan ruang (mis. VIP Room, Hall); filter per `cabang_id` |
| `TableMap` | Meja/ruang spesifik dalam sebuah group; `group_id` → FK TableGroup |
| `Deposit` | Transaksi saldo deposit; type `USE_RESERVATION` saat digunakan untuk bayar invoice |
| `Penjualan` | Order meja yang di-link ke reservasi via `reservation_id` |
| `Cabang` | Konfigurasi cabang termasuk Midtrans key & QRIS key |

### Field Kunci `reservasi_biodata`

| Field | Tipe | Keterangan |
|---|---|---|
| `no_reservasi` | string | Nomor unik reservasi (prefix `RSV` atau `EV`) |
| `event_name` | string | Jenis event; bisa dari daftar preset atau input manual |
| `customer_id` | int | FK → `customer.id` |
| `cabang_id` | int | Cabang tempat reservasi |
| `table_group_id` | int | FK → `table_group.id` (ruang/area) |
| `table_map_id` | int | FK → `table_map.id` (meja spesifik, opsional) |
| `date` | date | Tanggal acara (format `Y-m-d`) |
| `time` | time | Jam acara (format `H:i:s`) |
| `adults` / `children` | int | Jumlah tamu |
| `price` | decimal | Harga reservasi |
| `pic_name` | string | Nama PIC event |
| `note` | string | Catatan tambahan |
| `status_invoice` | int | 1=Uninvoiced, 2=Partial Invoiced, 3=Invoiced |
| `status_payment` | string | `Paid` / `Unpaid` (dari Midtrans public form) |
| `midtrans_status` | string | `Paid` / `Pending` / `Error` |

### Konstanta & Enum

```php
// Status invoice di reservasi_biodata
ReservasiBiodata::INV_UNINVOICED       = 1
ReservasiBiodata::INV_PARTIAL_INVOICED = 2
ReservasiBiodata::INV_INVOICED         = 3

// Status invoice AR
ReservasiInvoice::STATUS_UNPAID  = 'unpaid'
ReservasiInvoice::STATUS_PARTIAL = 'partial'
ReservasiInvoice::STATUS_PAID    = 'paid'

// Event name preset (dropdown + bisa input manual)
ReservasiBiodata::EVENT_NAME = ['Wedding', 'Anniversary', 'Gathering', 'Karaoke', 'Konser', 'Meeting']

// Prefix penomoran dokumen
ReservasiBiodata::PREFIX_NOTA = 'RSV'   // via POS app
// 'EV'                                 // via backend admin
ReservasiInvoice::INISIAL_NAME       = 'RI'
ReservasiInvoice::INISIAL_NAME_SALES = 'SI'
ReservasiPayment::INISIAL_NAME       = 'RP'
```

---

## Business Rules

### Validasi Bentrok Jadwal (Conflict Check)
Sebelum reservasi disimpan (create/update), dilakukan pengecekan:
- Jika `table_group_id` + `table_map_id` + `date` + `time` **sudah digunakan** reservasi lain → request ditolak
- Saat mode **edit**, record sendiri dikecualikan dari pengecekan
- Jika `table_group_id` tidak dipilih → pengecekan dilewati
- Implementasi: AJAX `POST reservation/check-conflict` dipanggil dari JS form `beforeSubmit`

### Aturan Edit Reservasi
Tombol **Edit** di datatable dinonaktifkan jika salah satu kondisi terpenuhi:
| Kondisi | Keterangan |
|---|---|
| `status_invoice != Uninvoiced` | Sudah diinvoice (partial/full) |
| `date < hari_ini - 5 hari` | Tanggal event sudah lewat lebih dari 5 hari |

### Aturan Hapus Reservasi
Tombol **Delete** di datatable dinonaktifkan jika salah satu kondisi terpenuhi:
| Kondisi | Keterangan |
|---|---|
| `status_invoice != Uninvoiced` | Sudah diinvoice |
| `deposit > 0` | Ada deposit yang sudah masuk |
| `paid > 0` | Ada pembayaran yang sudah dicatat |
| `remaining > 0` | Ada sisa tagihan |
| `date <= hari_ini` | Tanggal event sudah terlewati |

Selain via datatable, penghapusan dari **POS/API** juga diblokir jika:
- Sudah ada `ReservasiInvoiceDetail` terkait, ATAU
- Ada `Deposit` dengan type `TOPUP` yang linked ke reservasi ini

### Dependency TableGroup → TableMap
- Field `table_map_id` hanya bisa dipilih **setelah** `table_group_id` dipilih
- Saat `table_group_id` dikosongkan → `table_map_id` otomatis di-null-kan (form JS + server-side `actionUpdate`)
- Data `table_map_id` dimuat dinamis via AJAX (`GET reservation/ajax-table-map?groupId=X`)

### Penanganan Date & Time
- Form input: satu field `DateTimePicker` format `yyyy-mm-dd HH:ii` (24 jam)
- Saat submit: dipecah → `date = Y-m-d`, `time = H:i:s` (`:00` ditambahkan otomatis jika tidak ada detik)
- Saat edit: `date + time` digabung kembali sebelum dikirim ke view → `"2026-05-24 19:30"`

---

## API Terkait

### REST API — `api/v1`

Prefix base URL: `/api/v1/`

| Method | Endpoint | Controller | Keterangan |
|---|---|---|---|
| GET | `reservation/list` | `ReservationController::actionList` | List reservasi dari kemarin ke depan (per cabang), support search by name/hp |
| POST | `reservation/create` | `ReservationController::actionCreate` | Upsert reservasi — jika payload ada `id` = update, tanpa `id` = create baru |
| GET | `reservation/order/{id}` | `ReservationController::actionOrder` | List Penjualan (order) yang linked ke reservasi |
| POST | `reservation/delete` | `ReservationController::actionDelete` | Hapus reservasi dengan guard finansial |
| GET | `kasir/reservation` | `KasirController::actionReservation` | List reservasi hari ini ke depan untuk tampilan kasir; include `table_display_name`, `deposit`, `depositDetail` |

> `reservation/create` bersifat **upsert**: jika body JSON mengandung `id`, maka update; jika tidak ada, buat baru dan generate `no_reservasi`.

### Backend Admin Endpoints

| Method | Endpoint | Keterangan |
|---|---|---|
| GET | `reservation/ajax-table-map?groupId=X` | Mengembalikan `{results: [{id, text}]}` daftar `TableMap` berdasarkan `group_id`; dipakai oleh dependency dropdown form |
| POST | `reservation/check-conflict` | Validasi bentrok jadwal; payload: `table_group_id`, `table_map_id`, `date`, `time`, `id` (opsional untuk mode edit); return `{conflict: bool, message: string}` |

---

## Service / Usecase Utama

### API Usecases (`api/v1/usecases/reservation/`)

| File | Fungsi |
|---|---|
| `CreateUsecase` | Buat/update `ReservasiBiodata`; generate `no_reservasi` dengan prefix `RSV` hanya saat record baru; set `cabang_id`, `name`, `email` dari data Customer; jika `hp` null diisi dari Customer |
| `DeleteUsecase` | Hapus reservasi dengan guard: tolak jika sudah ada `ReservasiInvoiceDetail` atau `Deposit` type `TOPUP` terkait |

### Backend Usecases

| Namespace | File | Fungsi |
|---|---|---|
| `backend\usecases\reservasi_invoice` | `CreateUsecase` | Buat `ReservasiInvoice` + `ReservasiInvoiceDetail` per reservasi terpilih; update `status_invoice` tiap `ReservasiBiodata`; generate nomor `RI-`; dalam DB transaction |
| `backend\usecases\reservasi_invoice` | `CreateSalesUsecase` | Buat invoice kategori `sales` untuk penjualan paylater |
| `backend\usecases\reservasi_payment` | `CreateUsecase` | Buat `ReservasiPayment` + `ReservasiPaymentDetail`; update `paid_amount` dan `status` tiap invoice; kurangi saldo deposit jika `deposit_used > 0`; catat transaksi `Deposit` type `USE_RESERVATION`; dalam DB transaction |
| `backend\usecases\reservasi_payment` | `UpdateUsecase` | Update pembayaran yang sudah ada; rollback perubahan invoice status lama sebelum apply yang baru |
| `backend\usecases\reservasi_payment` | `DeleteUsecase` | Hapus payment dan rollback `paid_amount` + status invoice |

---

## Component Utama (Backend Views)

### Sub-modul Reservation (`backend/views/reservation/`)

| View | Keterangan |
|---|---|
| `reservation_list.php` | Datatable list semua reservasi; filter by status & tanggal (All/Soon/Past); kolom sortable: `no_reservasi`, `event_name`, `name`, `date`, `price`; kolom virtual (deposit/paid/remaining) tidak sortable |
| `_form.php` | Form create/edit reservasi; event_name via Select2 tags (preset + custom); date via DateTimePicker 24h; table_group → table_map dependency dropdown dengan AJAX; conflict check saat submit |
| `view.php` | Detail reservasi; tabel history deposit & pembayaran (via `datatables-deposit`) |
| `index.php` | Form booking publik (diakses customer langsung); disabled date dari DisableDate; slot waktu dari ReservasiPrice |
| `generate.php` | Konfigurasi slot waktu & harga (`ReservasiPrice`) + kelola `DisableDate` per cabang |
| `generate_update.php` | Edit slot harga |
| `payment_success.php` | Halaman sukses setelah pembayaran Midtrans; trigger kirim email konfirmasi |
| `notif-html.php` | Template HTML email notifikasi reservasi |

### Sub-modul Invoice AR (`backend/views/reservasi-invoice/`)

| View | Keterangan |
|---|---|
| `index.php` | Datatable list invoice AR |
| `_form.php` | Form create/edit invoice AR (event); pilih customer → muncul list reservasi yang belum diinvoice; satu invoice bisa cover banyak reservasi |
| `_form_sales.php` | Form invoice kategori sales (paylater) |
| `view.php` | Detail invoice AR |
| `index_aging.php` | Laporan aging piutang grouped by customer |
| `index_report.php` | Laporan outstanding piutang dengan filter customer |
| `index_history.php` | Customer statement — riwayat semua invoice per customer |

### Sub-modul Payment AR (`backend/views/reservasi-payment/`)

| View | Keterangan |
|---|---|
| `index.php` | Datatable list pembayaran AR |
| `_form.php` | Form create/edit pembayaran; pilih customer → muncul invoice yang belum lunas; support partial payment per invoice; support penggunaan deposit |
| `view.php` | Detail pembayaran beserta daftar invoice yang dibayar |

---

## Dependency Antar Module

```
ReservasiBiodata
  ├── depends on → Customer          (FK: customer_id)
  ├── depends on → TableGroup        (FK: table_group_id — ruang/area)
  ├── depends on → TableMap          (FK: table_map_id — meja spesifik, nullable)
  ├── depends on → Cabang            (cabang_id — konfigurasi Midtrans)
  ├── linked from → Penjualan        (Penjualan.reservation_id)
  ├── linked from → Deposit          (Deposit.reservation_id — deposit topup)
  └── linked from → ReservasiInvoiceDetail (reservasi_id)

TableGroup
  └── depends on → Cabang            (cabang_id)

TableMap
  └── depends on → TableGroup        (FK: group_id)

ReservasiInvoice
  ├── depends on → Customer          (FK: customer_id)
  ├── depends on → Cabang            (cabang_id)
  ├── has many → ReservasiInvoiceDetail
  └── has many → ReservasiPaymentDetail

ReservasiInvoiceDetail
  ├── belongs to → ReservasiInvoice
  └── belongs to → ReservasiBiodata  ← jembatan invoice ↔ reservasi (M:M)

ReservasiPayment
  ├── depends on → Customer          (FK: customer_id)
  ├── depends on → Cabang            (cabang_id)
  └── has many → ReservasiPaymentDetail

ReservasiPaymentDetail
  ├── belongs to → ReservasiPayment
  └── belongs to → ReservasiInvoice

ReservasiPrice
  └── depends on → Cabang            (cabang_id)

DisableDate
  └── depends on → Cabang            (cabang_id)

Deposit (type = USE_RESERVATION)
  ├── belongs to → Customer
  └── ref → ReservasiPayment.id (via refId)
```

### Integrasi Eksternal

| Sistem | Digunakan Untuk |
|---|---|
| **Midtrans** | Payment gateway untuk booking online publik (Snap Token via `actionBayar`); status dicek via `actionValidatePayment` menggunakan URL `url_status` |
| **Email (MailSender)** | Kirim konfirmasi reservasi setelah pembayaran sukses (`actionSuccess`) |

---

## RBAC Permissions

| Permission | Keterangan |
|---|---|
| `viewReservation` | Lihat list, detail, datatable, deposit history, serta akses endpoint AJAX (ajax-table-map, check-conflict) |
| `createReservation` | Buat reservasi baru |
| `updateReservation` | Edit reservasi — hanya aktif jika `status_invoice = Uninvoiced` dan `date >= hari_ini - 5 hari` |
| `deleteReservation` | Hapus reservasi — hanya aktif jika semua kondisi finansial & tanggal terpenuhi |
| `viewReservationInvoice` | Lihat invoice AR (termasuk aging, report, history) |
| `createReservationInvoice` | Buat invoice AR (event maupun sales) |
| `updateReservationInvoice` | Edit invoice AR — hanya jika status `unpaid` |
| `deleteReservationInvoice` | Hapus invoice AR — hanya jika status `unpaid` |
| `viewReservationPayment` | Lihat payment AR |
| `createReservationPayment` | Buat payment AR |
| `updateReservationPayment` | Edit payment AR — dibatasi berdasarkan tanggal input |
| `deleteReservationPayment` | Hapus payment AR — dibatasi berdasarkan tanggal input |

---

## Skema Penomoran

| Dokumen | Prefix | Generator | Contoh |
|---|---|---|---|
| Reservasi (publik/Midtrans) | `R{MD}{id}` | Manual di `actionBayar` | `R0524123` |
| Reservasi (backend admin) | `EV` | `MyHelper::generateMonthlyFaktur` | `EV-01-2605-001` |
| Reservasi (POS app) | `RSV` | `MyHelper::generateMonthlyNotaYearMonth` | `RSV-01-202605-001` |
| Invoice AR (event) | `RI` | `MyHelper::generateMonthlyFaktur` | `RI-01-2605-001` |
| Invoice AR (sales) | `SI` | `MyHelper::generateMonthlyFaktur` | `SI-01-2605-001` |
| Payment AR | `RP` | `MyHelper::generateMonthlyFaktur` | `RP-01-2605-001` |

---

## Catatan Implementasi

### Computed Properties pada ReservasiBiodata
Tiga property berikut **tidak** tersimpan di DB, dihitung runtime:

| Property | Sumber Data | Keterangan |
|---|---|---|
| `deposit` | `Deposit` table, type `TOPUP`, `reservation_id = id` | Total deposit yang masuk untuk reservasi ini |
| `depositUsed` | `Deposit` table, type bukan `TOPUP`, `reservation_id = id` | Total deposit yang sudah digunakan |
| `paid` | `Deposit` (USE) + `ReservasiInvoiceDetail` (paid invoices) | Total yang sudah dibayar dari semua sumber |

> Karena bukan kolom DB, kolom-kolom ini **tidak bisa digunakan untuk sorting SQL**. Di datatable, kolom `deposit`, `paid`, `remaining` dikecualikan dari `applyOrder` via daftar `$noSort`.

### Sorting Datatable
`applyOrder` hanya memproses kolom yang merupakan kolom DB nyata:
- **Sortable**: `event_name`, `name`, `email`, `hp`, `date`, `price`, `no_reservasi`, `adults`, dll.
- **Non-sortable** (virtual/computed): `deposit`, `paid`, `remaining`, `status_invoice`, `actions`
