# Fitur: Manajemen Customer, Reservasi & Account Receivable (AR)

## Gambaran Umum

Entitas **Customer** adalah pusat dari tiga modul besar dalam sistem ini: **Keuangan (Deposit)**, **Reservasi Event**, dan **Laporan Piutang (Account Receivable / AR)**. Customer menjadi penghubung antara aktivitas operasional (pemesanan slot event di tabel banquet, konsumsi di restoran) dan sisi finansial (invoice, pembayaran, aging piutang).

### Perbedaan Customer vs Staff

Tabel `customer` memiliki kolom `is_staff`:

| Nilai | Tipe     | Keterangan                                                                              |
|-------|----------|-----------------------------------------------------------------------------------------|
| `0`   | Customer | Pelanggan biasa. Kolom `foc_*` tidak diisi / diabaikan.                                 |
| `1`   | Staff    | Karyawan internal. Mendapat hak **Free of Charge (FOC / Allowance)** melalui `foc_*`.   |

Kolom `foc_*` yang relevan hanya berlaku untuk staff (`is_staff = 1`):

| Kolom                | Tipe    | Keterangan                                         |
|----------------------|---------|----------------------------------------------------|
| `foc_limit`          | decimal | Batas total allowance per bulan                    |
| `foc_used_this_month`| decimal | Akumulasi penggunaan allowance bulan berjalan       |
| `foc_reset_date`     | date    | Tanggal reset bulanan                              |
| `foc_limit_updated_at`| datetime | Timestamp update limit terakhir                  |
| `foc_limit_updated_by`| string  | User yang mengupdate limit                        |

---

## Skema Tabel Utama

### Tabel `customer`

| Kolom                 | Tipe     | Keterangan                                               |
|-----------------------|----------|----------------------------------------------------------|
| `id`                  | int PK   | Primary key                                              |
| `name`                | string   | Nama customer                                            |
| `email`               | string   | Email (unik, wajib)                                      |
| `phone`               | string   | Nomor HP                                                 |
| `card_no`             | string   | Nomor kartu member                                       |
| `no_ktp`              | string   | Nomor KTP / NIK                                          |
| `birthday`            | date     | Tanggal lahir                                            |
| `join_date`           | date     | Tanggal bergabung                                        |
| `address`             | string   | Alamat                                                   |
| `deposit`             | decimal  | Saldo deposit aktif (diperbarui secara real-time)        |
| `level`               | int      | Level keanggotaan                                        |
| `point`               | int      | Poin reward                                              |
| `status`              | string   | Status customer                                          |
| `is_staff`            | tinyint  | `0` = customer, `1` = staff                              |
| `no_service_charge`   | tinyint  | Flag pembebasan service charge                           |
| `no_tax`              | tinyint  | Flag pembebasan pajak                                    |
| `foc_limit`           | decimal  | Batas FOC/allowance (staff only)                         |
| `foc_used_this_month` | decimal  | FOC terpakai bulan ini (staff only)                      |
| `foc_reset_date`      | date     | Tanggal reset FOC (staff only)                           |
| `avatar`              | string   | Path/URL foto profil                                     |
| `parent_id`           | int      | Referensi parent customer (untuk hierarki)               |
| `created_at`          | datetime | Timestamp pembuatan                                      |
| `updated_at`          | datetime | Timestamp update                                         |

### Tabel `deposit`

| Kolom            | Tipe    | Keterangan                                                          |
|------------------|---------|---------------------------------------------------------------------|
| `id`             | int PK  | Primary key                                                         |
| `customer_id`    | int FK  | Referensi ke `customer.id`                                          |
| `cabang_id`      | int FK  | Referensi ke `cabang.id`                                            |
| `amount`         | decimal | Nilai transaksi (positif = top-up, negatif = penggunaan)            |
| `type`           | string  | `TOPUP` / `USE_RESERVATION` / `USE_RESTO` / `REFUND_RESTO`         |
| `category`       | string  | `RESTO` / `BANQUET`                                                 |
| `deposit_no`     | string  | Nomor referensi deposit (format: `AR-YYYYMM-XXXX`)                  |
| `deposit_date`   | date    | Tanggal transaksi deposit                                           |
| `reservation_id` | int FK  | Link ke `reservasi_biodata.id` (jika deposit terkait reservasi)     |
| `refId`          | string  | ID referensi transaksi asal (bill / payment)                        |
| `payment_method` | string  | Metode pembayaran top-up                                            |
| `bank_account`   | string  | Rekening bank                                                       |
| `description`    | string  | Keterangan                                                          |
| `created_at`     | datetime| Timestamp input                                                     |
| `deleted_at`     | datetime| Soft delete                                                         |
| `created_by`     | string  | User yang menginput                                                 |

---

## Modul 1: Keuangan (Deposit)

### Konsep Bisnis

Deposit adalah saldo prabayar milik customer yang digunakan sebagai alat pembayaran di restoran maupun untuk reservasi banquet. Setiap mutasi deposit dicatat di tabel `deposit` dengan `amount` positif (top-up) atau negatif (penggunaan), sementara `customer.deposit` menyimpan saldo agregat yang selalu diperbarui saat setiap transaksi deposit terjadi.

### Alur Bisnis Top-Up Deposit

```
[Staff/Kasir] Input Form Top-Up
       ↓
Validasi: customer_id, cabang_id, amount, deposit_date (required)
       ↓
Tentukan kategori:
  - Ada refId (reservasi) → category = BANQUET
  - Tidak ada refId       → category = RESTO
       ↓
Generate deposit_no: MyHelper::generateMonthlyFaktur(cabang_id, 'AR')
       ↓
BEGIN TRANSACTION
  1. INSERT INTO deposit (type=TOPUP, amount=+X, ...)
  2. UPDATE customer SET deposit = deposit + X WHERE id = customer_id
  3. saveIncrementMonthlyFaktur(cabang_id, 'AR')
COMMIT
       ↓
Customer.deposit bertambah
```

### Alur Penggunaan Deposit (Pembayaran Resto/Banquet)

```
[Kasir] Pilih metode bayar: DEPOSIT_RESTO / DEPOSIT_BANQUET
       ↓
Deposit::updateDeposit($penjualan)
       ↓
Cek: customer.deposit >= amount_dibayar?
  - Tidak → return error "Deposit tidak cukup"
  - Ya    → lanjut
       ↓
BEGIN TRANSACTION
  1. customer.deposit -= amount_dibayar
  2. INSERT INTO deposit (type=USE_RESTO, amount=-X, refId=bill.id)
COMMIT
```

### Alur Void / Refund Deposit

```
Deposit::updateDeposit($penjualan, isVoid=true)
       ↓
customer.deposit += amount (dikembalikan)
INSERT INTO deposit (type=REFUND_RESTO, amount=+X, refId=bill.id)
```

### Konstanta Tipe Deposit

| Konstanta           | Nilai              | Keterangan                        |
|---------------------|--------------------|-----------------------------------|
| `Deposit::TOPUP`    | `'TOPUP'`          | Top-up deposit oleh staff         |
| `Deposit::USE_RESERVATION` | `'USE_RESERVATION'` | Pemakaian untuk reservasi   |
| `Deposit::USE_RESTO`| `'USE_RESTO'`      | Pemakaian untuk bill restoran     |
| `Deposit::REFUND_RESTO` | `'REFUND_RESTO'` | Refund akibat void bill          |

### Deposit Detail Per Kategori

Method `Deposit::depositDetail($custId)` mengembalikan saldo deposit terpisah per kategori:

```php
// Return:
[
    'resto'   => float,  // SUM(amount) WHERE category='RESTO'
    'banquet' => float,  // SUM(amount) WHERE category='BANQUET'
]
```

### Fitur Soft Delete & Edit Deposit

- Deposit hanya bisa di-edit/delete dalam batas tanggal tertentu (`MyHelper::allowEditBasedOnDate`).
- Saat **edit**: `customer.deposit = (deposit - oldAmount) + newAmount`
- Saat **delete**: `customer.deposit -= amount` → deposit di-soft-delete

---

## Modul 2: Reservasi Event & Biodata

### Konsep Bisnis

Reservasi Event adalah pemesanan slot meja (table group) untuk sebuah acara (event) di tanggal dan waktu tertentu. Data pemesanan disimpan ke tabel `reservasi_biodata`. Reservasi dapat berasal dari:
1. **Publik** (via halaman web/Midtrans) — `ReservationController::actionIndex()`
2. **Backend** (oleh admin/staff) — `ReservationController::actionCreate()`

### Tabel `reservasi_biodata`

| Kolom             | Tipe      | Keterangan                                                       |
|-------------------|-----------|------------------------------------------------------------------|
| `id`              | int PK    | Primary key                                                      |
| `no_reservasi`    | string    | Nomor unik reservasi (prefix: `EV-YYYYMM-XXXX`, prefix RSV untuk public) |
| `customer_id`     | int FK    | Referensi ke `customer.id`                                       |
| `cabang_id`       | int FK    | Referensi ke `cabang.id`                                         |
| `event_name`      | string    | Nama event (wajib)                                               |
| `table_group_id`  | int FK    | Meja/area yang dipesan                                           |
| `table_map_id`    | int FK    | Denah meja                                                       |
| `date`            | date      | Tanggal event                                                    |
| `time`            | time      | Jam event                                                        |
| `adults`          | int       | Jumlah tamu dewasa (wajib)                                       |
| `children`        | int       | Jumlah anak-anak                                                 |
| `name`            | string    | Nama pemesan (di-copy dari customer.name saat create)            |
| `email`           | string    | Email pemesan (di-copy dari customer.email saat create)          |
| `hp`              | string    | Nomor HP pemesan                                                 |
| `price`           | decimal   | Harga total reservasi (wajib)                                    |
| `payment`         | decimal   | Jumlah yang telah dibayar via Midtrans                           |
| `status_payment`  | string    | `Paid` / `Unpaid`                                                |
| `status_invoice`  | int       | `1`=Uninvoiced, `2`=Partial Invoiced, `3`=Invoiced               |
| `midtrans_status` | string    | Status dari gateway Midtrans                                     |
| `url_status`      | string    | URL Midtrans untuk cek status pembayaran                         |
| `note`            | string    | Catatan tambahan                                                 |
| `pic_name`        | string    | Nama PIC                                                         |
| `created_at`      | datetime  | Timestamp input                                                  |
| `updated_at`      | datetime  | Timestamp update                                                 |

### Konstanta Status Invoice

| Konstanta                          | Nilai | Label              |
|------------------------------------|-------|--------------------|
| `ReservasiBiodata::INV_UNINVOICED` | `1`   | `Uninvoiced`       |
| `ReservasiBiodata::INV_PARTIAL_INVOICED` | `2` | `Partial Invoiced` |
| `ReservasiBiodata::INV_INVOICED`   | `3`   | `Invoiced`         |

### Alur Pemesanan Reservasi (Admin/Backend)

```
[Admin] Buka form create reservation
       ↓
Pilih customer_id, event_name, table_group_id, date+time, adults, price
       ↓
Validasi: event_name, customer_id, date, price, adults wajib
       ↓
model->name  = customer->name   (auto-populate)
model->email = customer->email  (auto-populate)
model->no_reservasi = MyHelper::generateMonthlyFaktur(cabang_id, 'EV')
       ↓
model->save()
       ↓
status_invoice = 1 (Uninvoiced) secara default
```

### Alur Pemesanan Reservasi (Publik via Web)

```
[Tamu] Pilih tanggal di kalender → Pilih waktu (dari ReservasiPrice)
       ↓
Cek kapasitas: ReservasiBiodata::sum('adults') WHERE date,time,status_payment='Paid'
  >= ReservasiPrice.max_reservasi? → tolak
       ↓
Isi form biodata → POST ke ReservationController::actionIndex()
       ↓
model->save() → status_payment='Unpaid'
       ↓
Midtrans: getSnapToken → redirect ke payment page
       ↓
[Callback] actionBayar() / actionValidatePayment()
  → status_payment='Paid', midtrans_status='Paid'
       ↓
Email konfirmasi dikirim via MailSender
```

### Relasi Customer ↔ ReservasiBiodata

```php
// Customer.php
$this->hasMany(ReservasiBiodata::class, ['customer_id' => 'id']);         // semua reservasi
$this->hasMany(ReservasiBiodata::class, ['customer_id' => 'id'])          // reservasi aktif
    ->andWhere(['>=', 'date', date('Y-m-d', strtotime("-2 day"))]);
```

### Kalkulasi Pembayaran Reservasi

Method `ReservasiBiodata::getPaid()` menghitung total yang sudah dibayar dari dua sumber:

```
paid = (deposit terpakai dari tabel deposit WHERE reservation_id = this.id)
     + (invoice detail yang sudah dibayar dari reservasi_invoice_detail WHERE status=paid)
```

### Update Status Invoice Reservasi

Dipanggil otomatis setiap kali invoice dibuat/diubah/dihapus:

```php
// ReservasiBiodata::updateInvoiceStatus()
$totalInvoiced = SUM(reservasi_invoice_detail.amount WHERE reservasi_id = this.id)
$totalInvoiced += $this->paid  // dari deposit

if ($totalInvoiced >= $this->price) → INV_INVOICED (3)
elif ($totalInvoiced > 0)           → INV_PARTIAL_INVOICED (2)
else                                → INV_UNINVOICED (1)
```

---

## Modul 3: Laporan Piutang / Account Receivable (AR)

### Konsep Bisnis

AR dalam sistem ini mengacu pada **tagihan kepada customer** yang belum dibayar penuh. Ada dua jalur AR:

1. **AR Reservasi/Banquet** → Invoice dibuat dari reservasi (`reservasi_invoice`, kategori `event`)
2. **AR Sales/Penjualan** → Invoice dibuat dari transaksi penjualan yang menggunakan metode `PAYLATER` (`reservasi_invoice`, kategori `sales`)

Kedua jalur menggunakan model yang sama (`ReservasiInvoice`) namun dengan category berbeda.

### Tabel `reservasi_invoice`

| Kolom           | Tipe     | Keterangan                                                       |
|-----------------|----------|------------------------------------------------------------------|
| `id`            | int PK   | Primary key                                                      |
| `invoice_number`| string   | Nomor invoice unik (prefix `RI` / `SI`)                          |
| `customer_id`   | int FK   | Referensi ke `customer.id`                                       |
| `cabang_id`     | int FK   | Referensi ke `cabang.id`                                         |
| `invoice_date`  | date     | Tanggal invoice diterbitkan (wajib)                              |
| `due_date`      | date     | Tanggal jatuh tempo (`>= invoice_date`, divalidasi)              |
| `amount`        | decimal  | Total tagihan                                                    |
| `paid_amount`   | decimal  | Total yang sudah dibayar                                         |
| `status`        | string   | `unpaid` / `partial` / `paid`                                    |
| `category`      | string   | `event` (dari reservasi) / `sales` (dari penjualan paylater)     |
| `note`          | string   | Catatan                                                          |
| `pj_ids`        | text/JSON| Array ID penjualan yang terkait (untuk sales invoice)            |
| `deleted_at`    | datetime | Soft delete                                                      |
| `created_by`    | string   | User pembuat                                                     |

### Tabel `reservasi_invoice_detail`

| Kolom          | Tipe    | Keterangan                                        |
|----------------|---------|---------------------------------------------------|
| `id`           | int PK  | Primary key                                       |
| `invoice_id`   | int FK  | Referensi ke `reservasi_invoice.id`               |
| `reservasi_id` | int FK  | Referensi ke `reservasi_biodata.id`               |
| `description`  | string  | Keterangan baris tagihan (wajib)                  |
| `amount`       | decimal | Nominal baris tagihan                             |

### Tabel `reservasi_payment_detail`

| Kolom          | Tipe    | Keterangan                                        |
|----------------|---------|---------------------------------------------------|
| `id`           | int PK  | Primary key                                       |
| `payment_id`   | int FK  | Referensi ke `reservasi_payment.id`               |
| `invoice_id`   | int FK  | Referensi ke `reservasi_invoice.id`               |
| `payment_date` | date    | Tanggal pembayaran                                |
| `amount`       | decimal | Nominal pembayaran baris ini                      |
| `ref`          | string  | Nomor referensi pembayaran                        |
| `note`         | string  | Catatan                                           |

### Alur Pembuatan Invoice AR (Reservasi/Event)

```
[Staf AR] Buka ReservasiInvoiceController::actionCreate()
       ↓
Pilih customer → auto-fetch daftar reservasi yang belum fully-invoiced
  (status_invoice != INV_INVOICED)
       ↓
Isi invoice_date, due_date, pilih reservasi + nominal per baris
       ↓
Validasi: due_date >= invoice_date (compareDates)
       ↓
BEGIN TRANSACTION (CreateUsecase)
  1. INSERT reservasi_invoice (status=unpaid, amount=SUM(detail.amount))
  2. INSERT reservasi_invoice_detail (satu baris per reservasi)
  3. UPDATE reservasi_biodata.status_invoice via updateInvoiceStatus()
COMMIT
```

### Alur Pembuatan Invoice AR (Sales/Paylater)

```
[Staf AR] Buka actionCreateSales()
       ↓
Pilih customer yang memiliki penjualan PAYLATER (inv_paylater_id IS NULL)
       ↓
CreateSalesUsecase::execute() → buat invoice dengan category='sales'
  + simpan pj_ids (array ID penjualan)
  + UPDATE penjualan.inv_paylater_id = invoice.id
       ↓
Saat invoice dihapus: Penjualan::updateAll(['inv_paylater_id' => null], ['id' => pj_ids])
```

### Alur Pembayaran Invoice (ReservasiPaymentController)

```
[Staf Keuangan] Buka ReservasiPaymentController::actionCreate()
       ↓
Pilih customer → daftar invoice outstanding milik customer
       ↓
Isi total pembayaran, method, referensi, tanggal
       ↓
CreateUsecase (reservasi_payment)
  → INSERT reservasi_payment (header)
  → INSERT reservasi_payment_detail per invoice (alokasi pembayaran)
  → UPDATE reservasi_invoice.paid_amount + status
     - paid_amount >= amount → status = 'paid'
     - paid_amount > 0       → status = 'partial'
     - else                  → status = 'unpaid'
```

---

### Laporan AR: Outstanding Balance

**Controller:** `ReservasiInvoiceController::actionReport()`
**Route:** `GET /reservasi-invoice/report?cid={customerHashId}`

Menampilkan **sisa tagihan** per customer yang belum lunas:

```sql
SELECT customer_id, SUM(amount) as amount, MIN(status) as status
FROM reservasi_invoice
WHERE cabang_id = ? AND status != 'paid' AND deleted_at IS NULL
GROUP BY customer_id
ORDER BY status ASC
-- Filter opsional: AND customer_id = ?
```

Output: tabel per customer dengan total outstanding dan status terkecil (unpaid > partial).

---

### Laporan AR: Aging Schedule

**Controller:** `ReservasiInvoiceController::actionAging()`
**Route:** `GET /reservasi-invoice/aging`

Mengelompokkan outstanding berdasarkan **umur piutang (hari sejak invoice dibuat)**:

**Implementasi di `DashboardCustomerController`:**

| Bucket       | Kondisi `created_at`                              |
|--------------|---------------------------------------------------|
| 1–30 hari    | `created_at >= NOW() - 30 days`                  |
| 31–60 hari   | `NOW()-60 days <= created_at < NOW()-30 days`    |
| 61–90 hari   | `NOW()-90 days <= created_at < NOW()-60 days`    |
| 91–120 hari  | `NOW()-120 days <= created_at < NOW()-90 days`   |
| 120+ hari    | `created_at < NOW() - 120 days`                  |

Formula outstanding per bucket: `SUM(amount - paid_amount)`

**Route Dashboard AJAX:** `GET /dashboard-customer/ajax-aging-distribution-receivable`

Di tabel Aging (`actionAging`), data digroup per `customer_id`:
```sql
SELECT customer_id, SUM(amount) as amount
FROM reservasi_invoice
WHERE cabang_id = ? AND status != 'paid' AND deleted_at IS NULL
GROUP BY customer_id
```

View `index_aging` menampilkan aging ini dalam format tabel dengan aging category per-baris.

---

### Laporan AR: Customer Statement

**Controller:** `ReservasiInvoiceController::actionHistory()`
**Route:** `GET /reservasi-invoice/history?cid={customerHashId}&all={0|1}`

Rekap histori mutasi tagihan dan pembayaran per customer:

```sql
SELECT customer_id, SUM(amount) as amount, MAX(invoice_date) as last_invoice_date
FROM reservasi_invoice
WHERE cabang_id = ?
  [AND status != 'paid']      -- jika parameter all tidak ada
  [AND customer_id = ?]       -- jika cid dipilih
GROUP BY customer_id
ORDER BY last_invoice_date DESC
```

- **Parameter `all=1`**: tampilkan semua invoice termasuk yang sudah lunas (full history).
- **Parameter `cid`**: filter satu customer spesifik (hashId → id).
- View `index_history` menampilkan rincian per invoice + payment history untuk masing-masing customer.

---

### View SQL: `v_penjualan_unpaid_paylater`

Model `VPenjualanUnpaidPaylater` adalah **view database** yang mengagregasi transaksi penjualan dengan metode PAYLATER yang belum lunas:

| Kolom          | Keterangan                                       |
|----------------|--------------------------------------------------|
| `customer_id`  | Customer pemilik tagihan                         |
| `pembeli`      | Nama pembeli                                     |
| `no_faktur`    | Nomor faktur penjualan                           |
| `tanggal`      | Tanggal transaksi                                |
| `reservation_id`| Terkait reservasi (jika ada)                   |
| `total`        | Total transaksi                                  |
| `unpaid`       | Sisa yang belum dibayar                          |
| `status`       | `lunas` / `belum`                                |

Digunakan oleh:
- `Customer::totalUnpaidPaylater($customerId)` — menghitung total tagihan paylater belum lunas per customer
- `DashboardCustomerController::actionAjaxCustomersWithUnpaidPaylater()` — dashboard piutang paylater
- `api/v1/controllers/CustomerController::actionList()` — field `total_unpaid` di response API

---

## Komponen Kode & API Reference (Yii2 MVC)

### A. Web Form Endpoints (Backend)

#### Modul Customer (CRUD)

| Action                        | Method   | URL Pattern                           | Permission              |
|-------------------------------|----------|---------------------------------------|-------------------------|
| List customer                 | GET      | `/customer/index`                     | `viewCustomer`          |
| Detail customer               | GET      | `/customer/view/{hashId}`             | `viewCustomer`          |
| Deposit report (semua cust.)  | GET      | `/customer/deposit-report`            | `viewCustomer`          |
| Datatables customer           | GET/POST | `/customer/datatables`                | `viewCustomer`          |
| Datatables deposit customer   | GET/POST | `/customer/datatables-deposit`        | `viewCustomer`          |
| Datatables reservasi customer | GET/POST | `/customer/datatables-reservation`    | `viewCustomer`          |
| AJAX search customer          | GET      | `/customer/ajax-get-all?nama={q}`     | `viewCustomer`          |
| Create customer               | GET/POST | `/customer/create`                    | `createCustomer`        |
| Import customer (form)        | GET      | `/customer/import`                    | `createCustomer`        |
| Upload preview import         | POST     | `/customer/upload-preview`            | `createCustomer`        |
| Proses import                 | POST     | `/customer/process-import`            | `createCustomer`        |
| Download template import      | GET      | `/customer/download-template`         | `createCustomer`        |
| Export baris error            | GET      | `/customer/export-errors?token={t}`   | `createCustomer`        |
| Edit customer                 | GET/POST | `/customer/update/{hashId}`           | `updateCustomer`        |
| Delete customer               | POST     | `/customer/delete/{hashId}`           | `deleteCustomer`        |

**Catatan Delete:** Customer tidak dapat dihapus jika memiliki deposit (`deposit > 0`) atau memiliki reservasi.

---

#### Modul Deposit (CRUD)

| Action              | Method   | URL Pattern                          | Permission          |
|---------------------|----------|--------------------------------------|---------------------|
| List deposit        | GET      | `/deposit/index`                     | `viewDeposit`       |
| Datatables          | GET/POST | `/deposit/datatables`                | `viewDeposit`       |
| View detail         | GET      | `/deposit/view/{hashId}`             | `viewDeposit`       |
| View PDF            | GET      | `/deposit/pdf/{hashId}`              | `viewDeposit`       |
| Create top-up       | GET/POST | `/deposit/create?id={customerId}`    | `createDeposit`     |
| Edit top-up         | GET/POST | `/deposit/update/{hashId}`           | `updateDeposit`     |
| Delete top-up       | POST     | `/deposit/delete/{hashId}`           | `deleteDeposit`     |

**Catatan:** Edit/Delete deposit dibatasi oleh tanggal via `MyHelper::allowEditBasedOnDate`.

---

#### Modul Reservasi Event

| Action                      | Method   | URL Pattern                                | Permission              |
|-----------------------------|----------|--------------------------------------------|-------------------------|
| List reservasi (backend)    | GET      | `/reservation/list?sts={all|paid|unpaid}`  | `viewReservation`       |
| Datatables reservasi        | GET/POST | `/reservation/datatables`                  | `viewReservation`       |
| Datatables deposit reservasi| GET/POST | `/reservation/datatables-deposit`          | `viewReservation`       |
| View detail reservasi       | GET      | `/reservation/view/{hashId}`               | `viewReservation`       |
| Create reservasi            | GET/POST | `/reservation/create`                      | `createReservation`     |
| Edit reservasi              | GET/POST | `/reservation/update/{hashId}`             | `updateReservation`     |
| Delete reservasi            | POST     | `/reservation/delete/{hashId}`             | `deleteReservation`     |
| Form reservasi publik       | GET/POST | `/reservation/index`                       | public (no auth)        |
| Cek ketersediaan jam        | GET      | `/reservation/jam?date=&cabang_id=`        | `viewReservation`       |
| Validasi pembayaran Midtrans| POST(AJAX)| `/reservation/validate-payment`           | `viewReservation`       |
| Konfirmasi bayar Midtrans   | POST(AJAX)| `/reservation/bayar`                      | `viewReservation`       |
| Generate pricing            | GET/POST | `/reservation/generate`                    | `@` (any logged in)     |

**Catatan Edit/Delete:** Hanya bisa saat `status_invoice == INV_UNINVOICED (1)`.

---

#### Modul AR: Invoice Reservasi

| Action                          | Method   | URL Pattern                                     | Permission                   |
|---------------------------------|----------|-------------------------------------------------|------------------------------|
| List invoice AR                 | GET      | `/reservasi-invoice/index`                      | `viewReservationInvoice`     |
| Datatables invoice              | GET/POST | `/reservasi-invoice/datatables`                 | `viewReservationInvoice`     |
| View invoice                    | GET      | `/reservasi-invoice/view/{hashId}`              | `viewReservationInvoice`     |
| View sales invoice              | GET      | `/reservasi-invoice/view-sales/{hashId}`        | `viewReservationInvoice`     |
| Laporan Outstanding AR          | GET      | `/reservasi-invoice/report?cid={hashId}`        | `viewReservationInvoice`     |
| Laporan Aging AR                | GET      | `/reservasi-invoice/aging`                      | `viewReservationInvoice`     |
| Customer Statement              | GET      | `/reservasi-invoice/history?cid={hashId}&all=1` | `viewReservationInvoice`     |
| AJAX search customer (event)    | GET      | `/reservasi-invoice/ajax-get-customer?nama=`    | `viewReservationInvoice`     |
| AJAX search customer (sales)    | GET      | `/reservasi-invoice/ajax-get-customer-has-sales?nama=` | `viewReservationInvoice` |
| Create invoice (event)          | GET/POST | `/reservasi-invoice/create?customer_id=`        | `createReservationInvoice`   |
| Create invoice (sales/paylater) | GET/POST | `/reservasi-invoice/create-sales?customer_id=`  | `createSalesInvoice`         |
| Edit invoice                    | GET/POST | `/reservasi-invoice/update/{hashId}`            | `updateReservationInvoice`   |
| Delete invoice                  | POST     | `/reservasi-invoice/delete/{hashId}`            | `deleteReservationInvoice`   |

**Catatan Edit/Delete Invoice:** Hanya saat `status == 'unpaid'` dan bukan kategori `sales`.

---

#### Modul AR: Payment Reservasi

| Action               | Method   | URL Pattern                                  | Permission                   |
|----------------------|----------|----------------------------------------------|------------------------------|
| List payment         | GET      | `/reservasi-payment/index`                   | `viewReservationPayment`     |
| Datatables payment   | GET/POST | `/reservasi-payment/datatables`              | `viewReservationPayment`     |
| View payment         | GET      | `/reservasi-payment/view/{hashId}`           | `viewReservationPayment`     |
| Create payment       | GET/POST | `/reservasi-payment/create`                  | `createReservationPayment`   |
| Edit payment         | GET/POST | `/reservasi-payment/update/{hashId}`         | `updateReservationPayment`   |
| Delete payment       | POST     | `/reservasi-payment/delete/{hashId}`         | `deleteReservationPayment`   |
| AJAX get customer    | GET      | `/reservasi-payment/ajax-get-customer?nama=` | `@`                          |

---

### B. REST API Endpoints

Base URL: `/api/v1/`

#### Customer API

| Method | Endpoint                            | Auth       | Keterangan                                        |
|--------|-------------------------------------|------------|---------------------------------------------------|
| POST   | `customer/create`                   | No auth    | Registrasi customer baru via mobile app           |
| POST   | `customer/update`                   | Required   | Update profil customer (foto, nama, phone)        |
| GET    | `customer/view/{id}`                | Required   | Detail customer + data User                       |
| GET    | `customer/list?k={keyword}&withStaff={0|1}` | Required | Pencarian customer / customer+staff         |
| GET    | `whoami`                            | Required   | Info user aktif yang sedang login                 |
| POST   | `customer/updatetoken`              | Required   | Update FCM token untuk push notification          |

#### Kasir API (Customer Operations)

| Method | Endpoint                                       | Auth     | Keterangan                          |
|--------|------------------------------------------------|----------|-------------------------------------|
| POST   | `kasir/customer-create`                        | Required | Buat customer baru dari kasir app   |
| POST   | `kasir/customer-edit`                          | Required | Edit data customer dari kasir app   |
| DELETE | `kasir/customer-delete`                        | Required | Hapus customer dari kasir app       |
| POST   | `kasir/deposit-create`                         | Required | Top-up deposit customer             |
| GET/PUT| `kasir/deposit-edit/{id}`                      | Required | Edit deposit                        |
| DELETE | `kasir/deposit-delete`                         | Required | Hapus deposit                       |
| GET    | `kasir/deposit-list`                           | Required | List deposit customer               |
| GET    | `kasir/deposit-history`                        | Required | Histori deposit customer            |
| GET    | `kasir/customer-unpaid-paylater/{customerId}`  | Required | Outstanding paylater per customer   |

#### Reservation API

| Method | Endpoint                     | Auth     | Keterangan                     |
|--------|------------------------------|----------|--------------------------------|
| POST   | `reservation/create`         | Required | Buat reservasi baru            |
| DELETE | `reservation/delete`         | Required | Hapus reservasi                |
| GET    | `reservation/list`           | Required | List reservasi customer aktif  |
| GET    | `reservation/order/{id}`     | Required | Detail order reservasi         |

---

### C. Contoh Request & Response

#### POST `customer/create` — Registrasi Customer Baru

**Request Body (JSON):**
```json
{
  "Customer": {
    "name": "John Doe",
    "email": "john@example.com",
    "phone": "081234567890",
    "reg_type": "regular",
    "password": "secret123",
    "gender": "M",
    "birth_date": "1990-01-15"
  }
}
```

**Response Sukses:**
```json
{
  "date": "2026-05-23 10:00:00",
  "name": "Customer",
  "message": "Create",
  "code": 1,
  "status": 200,
  "data": null
}
```

**Response Error:**
```json
{
  "status": 500,
  "code": 0,
  "message": "Customer Create Failed",
  "data": {
    "email": ["Email has already been taken."]
  }
}
```

---

#### GET `customer/list?k=john&withStaff=0` — Pencarian Customer

**Response:**
```json
{
  "date": "2026-05-23 10:00:00",
  "name": "Customers",
  "message": "List",
  "code": 1,
  "status": 200,
  "data": [
    {
      "id": 42,
      "name": "John Doe",
      "card_no": "CARD001",
      "no_ktp": "3201...",
      "phone": "081234567890",
      "birthday": "1990-01-15",
      "join_date": "2024-01-01",
      "email": "john@example.com",
      "deposit": 500000,
      "depositDetail": { "resto": 200000, "banquet": 300000 },
      "allowance": 0,
      "no_service_charge": 0,
      "no_tax": 0,
      "address": "Jl. Contoh No. 1",
      "created_at": "2024-01-01 09:00:00",
      "reservations": [
        {
          "id": 10,
          "event_name": "Birthday Party",
          "no_reservasi": "EV-202405-0001",
          "date": "2026-06-01"
        }
      ],
      "total_unpaid": 0
    }
  ]
}
```

**Catatan:** Field `allowance` hanya terisi jika `is_staff = 1` (`foc_limit - foc_used_this_month`). Untuk customer biasa selalu `0`.

---

#### POST `kasir/deposit-create` — Top-Up Deposit

**Request Body (JSON):**
```json
{
  "Deposit": {
    "customer_id": 42,
    "amount": 500000,
    "deposit_date": "2026-05-23",
    "payment_method": "Transfer BCA",
    "refId": null
  }
}
```

**Response Sukses:**
```json
{
  "status": 200,
  "code": 1,
  "message": "Deposit created"
}
```

---

#### POST `customer/import` → Upload Preview (Web Form)

**Request:** `multipart/form-data`, field `excel_file` (.xlsx / .xls)

**Kolom Excel yang Didukung:**

| Kolom      | Wajib | Alias yang Diterima                                         |
|------------|-------|-------------------------------------------------------------|
| `card_no`  | Ya    | card no, cardno, member card, card id                       |
| `name`     | Ya    | nama, nama lengkap, full name, customer name                |
| `phone`    | Ya    | no hp, telepon, hp, handphone, no telp                      |
| `email`    | Tidak | e-mail, email address                                       |
| `no_ktp`   | Tidak | ktp, nik, nomor ktp, id card                                |
| `birthday` | Tidak | birth date, tgl lahir, tanggal lahir, dob, date of birth    |
| `join_date`| Tidak | join date, tanggal bergabung, registration date             |
| `address`  | Tidak | alamat, addr, alamat lengkap                                |

**Response Preview:**
```json
{
  "success": true,
  "token": "abc123...",
  "rows": [
    {
      "row_num": 2,
      "data": { "card_no": "CARD001", "name": "John Doe", ... },
      "errors": [],
      "status": "valid"
    },
    {
      "row_num": 3,
      "data": { "card_no": "CARD002", "name": "Jane" },
      "errors": ["name: \"Jane\" → minimal 3 karakter"],
      "status": "error"
    }
  ],
  "summary": { "total": 2, "valid": 1, "error": 1 }
}
```

---

#### Dashboard AR — AJAX Endpoints

| Method | URL                                                      | Keterangan                          |
|--------|----------------------------------------------------------|-------------------------------------|
| GET    | `/dashboard-customer/index`                              | Halaman dashboard utama             |
| GET    | `/dashboard-customer/ajax-kpi-cards`                     | KPI: total customer, reservasi, deposit, revenue |
| GET    | `/dashboard-customer/ajax-top-customers-by-deposit`      | Top 5 customer deposit terbesar     |
| GET    | `/dashboard-customer/ajax-upcoming-events`               | 5 event terdekat                    |
| GET    | `/dashboard-customer/ajax-recent-deposits`               | 5 top-up deposit terbaru            |
| GET    | `/dashboard-customer/ajax-customer-trend`                | Tren pertumbuhan customer (6 bulan) |
| GET    | `/dashboard-customer/ajax-reservation-status`            | Breakdown status invoice reservasi  |
| GET    | `/dashboard-customer/ajax-deposit-trend`                 | Tren top-up deposit (6 bulan)       |
| GET    | `/dashboard-customer/ajax-low-balance-customers`         | Customer deposit < threshold (default 500rb) |
| GET    | `/dashboard-customer/ajax-kpi-cards-receivable`          | KPI AR: outstanding, overdue, due week, avg payment days |
| GET    | `/dashboard-customer/ajax-aging-distribution-receivable` | Distribusi aging 1-30, 31-60, 61-90, 91-120, 120+ |
| GET    | `/dashboard-customer/ajax-alerts-receivable`             | Alert: overdue, due week, large outstanding |
| GET    | `/dashboard-customer/ajax-payment-trend-receivable`      | Tren pembayaran vs outstanding (6 bulan) |
| GET    | `/dashboard-customer/ajax-outstanding-table-receivable`  | Tabel invoice outstanding (max 50 row) |
| GET    | `/dashboard-customer/ajax-kpi-cards-foc`                 | KPI FOC: allocated, used, avg, remaining |
| GET    | `/dashboard-customer/ajax-foc-trend`                     | Tren penggunaan FOC (6 bulan)       |
| GET    | `/dashboard-customer/ajax-top-customers-by-foc`          | Top 10 customer penggunaan FOC      |
| GET    | `/dashboard-customer/ajax-foc-alerts`                    | Alert customer hampir habis FOC (>= 80%) |
| GET    | `/dashboard-customer/ajax-customers-with-unpaid-paylater`| Customer dengan utang paylater      |

---

## Modul Tambahan Temuan Claude

### Modul 4: Free of Charge (FOC) / Staff Allowance

**File-file terkait:**
- [common/models/Customer.php](../../common/models/Customer.php) — field `foc_*`, method `getFocBalance()`, `canUseFoc()`, `updateFocUsage()`, `getFocTransactions()`
- [common/models/FocTransaction.php](../../common/models/FocTransaction.php) — tabel `foc_transaction`
- [backend/controllers/DashboardCustomerController.php](../../backend/controllers/DashboardCustomerController.php) — dashboard FOC analytics

**Konsep Bisnis:**

FOC (Free of Charge) adalah fasilitas **allowance makan gratis per bulan** yang diberikan kepada **staff** (`is_staff = 1`). Customer biasa tidak memiliki limit FOC. Setiap penggunaan allowance saat membeli makanan (metode pembayaran `METHOD_ALLOWANCE`) dicatat ke tabel `foc_transaction` dan menambah `customer.foc_used_this_month`.

**Tabel `foc_transaction`:**

| Kolom              | Keterangan                                         |
|--------------------|----------------------------------------------------|
| `customer_id`      | Staff yang menggunakan FOC                          |
| `cabang_id`        | Cabang tempat transaksi                            |
| `amount`           | Nominal (positif=topup, negatif=pemakaian)         |
| `type`             | `TOPUP` / `USE_PENJUALAN` / `REFUND` / `MONTHLY_RESET` |
| `foc_no`           | Nomor referensi (format `ALC-YYYYMM-XXXX` / `FOC-YYYYMM-XXXX`) |
| `transaction_date` | Tanggal transaksi                                  |
| `refId`            | ID penjualan yang menggunakan FOC                  |

**Alur Penggunaan FOC:**

```
[Kasir] Pilih metode bayar: METHOD_ALLOWANCE
       ↓
FocTransaction::useFoc($penjualan, isVoid=false)
       ↓
Cek: customer.foc_limit - customer.foc_used_this_month >= amount?
  - Tidak → error "Allowance limit tidak mencukupi"
  - Ya    → lanjut
       ↓
customer.foc_used_this_month += amount
customer.save()
       ↓
INSERT foc_transaction (type=USE_PENJUALAN, amount=-X, refId=penjualan.id)
foc_no = generateMonthlyFaktur(cabang_id, 'ALC')
```

**Alur Void/Refund FOC:**
```
FocTransaction::useFoc($penjualan, isVoid=true)
  → customer.foc_used_this_month -= amount (min 0)
  → INSERT foc_transaction (type=REFUND, amount=+X)
```

**Alur Top-Up FOC Limit:**
```
FocTransaction::addFocLimit($customerId, $cabangId, $amount, $description)
  → customer.foc_limit += amount
  → INSERT foc_transaction (type=TOPUP, amount=+X)
  → foc_no = generateMonthlyFaktur(cabang_id, 'FOC')
```

**Caching FOC Balance:**
```php
// FocTransaction::focDetail($custId)
// Cache dengan DbDependency pada customer.updated_at, TTL 1 jam
$cacheKey = CACHE_FOC_BALANCE_PREFIX . $custId;
$result = Yii::$app->cache->get($cacheKey);
// ...
Yii::$app->cache->set($cacheKey, $result, 3600, $dependency);
```

**Dashboard FOC Alerts:**
- Customer dengan `foc_used_this_month / foc_limit >= 80%` ditandai sebagai Warning
- Customer dengan >= 90% ditandai sebagai Critical/Danger
- Customer dengan >= 100% (exceeded) ditandai sebagai "FOC limit exceeded!"

---

### Modul 5: Import Customer (Bulk)

**Controller:** `backend/controllers/CustomerController.php` — `actionUploadPreview()`, `actionProcessImport()`, `actionDownloadTemplate()`, `actionExportErrors()`

**Alur:**

```
[Admin] Download template Excel → isi data → Upload
       ↓
actionUploadPreview()
  → Parse xlsx/xls via PhpSpreadsheet
  → Auto-detect kolom dengan alias (name/nama, phone/hp, dll.)
  → Validasi per baris: name (min 3 char), card_no (no spasi), phone (5-18 digit),
    email (format valid), birthday (tidak > hari ini), join_date (>= birthday)
  → Cek duplikat dalam file (email, phone, card_no, no_ktp)
  → Cek duplikat ke database (batch query)
  → Simpan hasil ke session (TTL 1 jam) dengan token unik
  → Return preview rows + summary
       ↓
[Admin] Review preview → Confirm import
       ↓
actionProcessImport(token)
  → Ambil valid rows dari session
  → batchInsert ke tabel customer (is_staff=0)
  → Commit
       ↓
actionExportErrors(token)
  → Export baris error ke xlsx dengan highlight merah
```

---

### Modul 6: Dashboard Customer & AR Analytics

**Controller:** [backend/controllers/DashboardCustomerController.php](../../backend/controllers/DashboardCustomerController.php)
**Route:** `/dashboard-customer/index`
**Permission:** `viewReservation`

Dashboard tiga tab yang menyajikan:

1. **Tab Customer & Reservasi:**
   - KPI: total customer, reservasi aktif, saldo deposit total, revenue reservasi
   - Grafik tren pertumbuhan customer (6 bulan terakhir)
   - Grafik tren top-up deposit (6 bulan)
   - Tabel top 5 customer deposit terbesar
   - Tabel 5 upcoming events terdekat
   - Alert customer deposit rendah (< threshold)

2. **Tab Piutang (AR Receivable):**
   - KPI: total outstanding, overdue count, due this week, avg days to pay
   - Pie chart distribusi aging (5 bucket)
   - Line chart tren payment received vs outstanding (6 bulan)
   - Tabel invoice outstanding terlama (50 baris)
   - Alert: overdue invoices, due this week, large outstanding (> 50jt)
   - Tabel customer dengan unpaid paylater

3. **Tab FOC/Allowance:**
   - KPI: total allocated, total used, avg per customer, remaining
   - Bar chart tren penggunaan FOC (6 bulan)
   - Tabel top 10 customer by FOC usage + usage percentage
   - Alert customer yang FOC-nya hampir habis (>= 80%)

---

### Modul 7: Customer dari Kasir API (Usecases)

**File-file terkait:**
- [api/v1/usecases/kasir/CustomerCreateUsecase.php](../../api/v1/usecases/kasir/CustomerCreateUsecase.php)
- [api/v1/usecases/kasir/CustomerEditUsecase.php](../../api/v1/usecases/kasir/CustomerEditUsecase.php)
- [api/v1/usecases/kasir/CustomerDeleteUsecase.php](../../api/v1/usecases/kasir/CustomerDeleteUsecase.php)
- [api/v1/usecases/kasir/DepositCreateUsecase.php](../../api/v1/usecases/kasir/DepositCreateUsecase.php)
- [api/v1/usecases/kasir/DepositDeleteUsecase.php](../../api/v1/usecases/kasir/DepositDeleteUsecase.php)
- [api/v1/usecases/kasir/DepositEditUsecase.php](../../api/v1/usecases/kasir/DepositEditUsecase.php)

**Pola Arsitektur:** Kasir API menggunakan pola **Usecase** (terpisah dari controller) untuk enkapsulasi logika bisnis. `KasirController` mendelegasikan ke masing-masing Usecase class.

**CustomerCreateUsecase:**
- Load data Customer dari request body
- Auto-generate `card_no` dengan format `GSM-{cabang}` jika tidak diisi
- Validasi → save → return atau throw HttpException

**DepositCreateUsecase:**
- Sama persis dengan `DepositController::actionCreate()` di backend
- Mendukung link ke reservasi (category BANQUET jika ada `refId`)
- Generate `deposit_no` dengan prefix `AR`

---

### Modul 8: API Reservation (Mobile/Kasir App)

**Controller:** [api/v1/controllers/ReservationController.php](../../api/v1/controllers/ReservationController.php)

| Endpoint                     | Keterangan                                                       |
|------------------------------|------------------------------------------------------------------|
| `POST reservation/create`    | Buat reservasi, validasi slot, hitung harga dari `ReservasiPrice`|
| `DELETE reservation/delete`  | Hapus reservasi milik customer aktif                             |
| `GET reservation/list`       | List reservasi customer aktif yang login                         |
| `GET reservation/order/{id}` | Detail satu reservasi + deposit terkait                          |

**Route `kasir/reservation`:** Digunakan kasir untuk melihat/memilih reservasi customer yang sedang di meja, kemudian menghubungkan bill penjualan ke reservasi tersebut.

---

### Modul 9: Penjualan dengan Metode Deposit & Paylater

**File terkait:** `common/models/Deposit.php` — `updateDeposit()`, `api/v1/usecases/penjualan/PaymentUsecase.php`

Saat customer melakukan pembayaran di restoran (`Penjualan`), sistem mendukung metode:

| Metode Bayar            | Konstanta                      | Efek pada Deposit                         |
|-------------------------|--------------------------------|--------------------------------------------|
| Deposit Resto           | `METHOD_DEPOSIT_RESTO`         | `customer.deposit -= amount`, INSERT deposit USE_RESTO |
| Deposit Banquet         | `METHOD_DEPOSIT_BANQUET`       | `customer.deposit -= amount`, INSERT deposit USE_RESTO (cat=BANQUET) |
| Allowance/FOC           | `METHOD_ALLOWANCE`             | `customer.foc_used_this_month += amount`, INSERT foc_transaction |
| Pay Later               | `METHOD_PAYLATER`              | Tidak ada perubahan deposit/FOC. Dicatat sebagai hutang → masuk AR |
| Split payment           | `bayar_detail` JSON array      | Kombinasi beberapa metode, termasuk deposit/FOC/paylater |

**Catatan penting:** Penjualan dengan PAYLATER akan muncul di `v_penjualan_unpaid_paylater` dan bisa dibuatkan invoice Sales AR (`ReservasiInvoice` category=`sales`) untuk penagihan formal.

---

## Ringkasan Relasi Antar Entitas

```
Customer (1)
  ├── (n) Deposit             — saldo top-up & pemakaian (BANQUET/RESTO)
  ├── (n) FocTransaction      — riwayat allowance staff (is_staff=1 only)
  ├── (n) ReservasiBiodata    — pemesanan slot event
  │       └── (n) ReservasiInvoiceDetail → (1) ReservasiInvoice
  │                                            └── (n) ReservasiPaymentDetail → (1) ReservasiPayment
  ├── (n) ReservasiInvoice    — invoice AR langsung ke customer
  │       ├── category='event'  → dari reservasi
  │       └── category='sales'  → dari penjualan paylater (pj_ids)
  ├── (1) User                — akun login yang terhubung ke customer
  └── (n) VPenjualanUnpaidPaylater — VIEW: tagihan paylater belum lunas
```
