MIF_E31232103/DOKUMENTASI_TAHAP8_BACKEND_...

447 lines
16 KiB
Markdown

# DOKUMENTASI TAHAP 8: BACKEND PROMO + CRM
## Status Implementasi
**COMPLETE** - Semua fitur Tahap 8 sudah diimplementasikan dan siap testing.
---
## 1. Overview Tahap 8
Tahap 8 menambahkan dua modul besar:
1. **Promo Module**: CRUD promo per rental company dengan validasi ketat, integrasi booking flow, dan perhitungan diskon otomatis
2. **CRM Module**: Dashboard customer dengan tracking loyalitas, riwayat booking, reviews, dan targeting promo
### Aturan Bisnis Utama
- **Loyalitas**: Customer dianggap setia jika telah menyelesaikan ≥2 booking di rental yang sama
- **Promo Validation**: Validasi ketat di backend dengan transaction locking untuk prevent race condition
- **Diskon Calculation**: 100% dihitung di backend, frontend hanya menampilkan estimasi
- **Access Control**: Admin rental hanya bisa mengelola promo & customer miliknya
---
## 2. Database Layer
### Migrations Created
#### 1. `database/migrations/2026_04_21_000011_create_promos_table.php`
```sql
CREATE TABLE promos (
id BIGINT PRIMARY KEY
rental_company_id BIGINT (FK to rental_companies)
title VARCHAR (255)
promo_code VARCHAR (50) UNIQUE INDEX
description TEXT NULLABLE
discount_type ENUM ('percent', 'fixed')
discount_value DECIMAL (10,2)
min_transaction DECIMAL (12,2) NULLABLE
start_date DATETIME
end_date DATETIME
quota INT NULLABLE (unlimited jika null)
used_count INT DEFAULT 0
loyal_only BOOLEAN DEFAULT 0
status ENUM ('active', 'inactive')
timestamps
INDEXES:
- (rental_company_id, status)
- (rental_company_id, start_date, end_date)
)
```
#### 2. `database/migrations/2026_04_21_000012_add_promo_foreign_key_to_bookings_table.php`
```sql
ALTER TABLE bookings ADD COLUMN promo_id BIGINT NULLABLE
ADD FOREIGN KEY (promo_id) REFERENCES promos(id) ON DELETE SET NULL
```
---
## 3. Model Layer
### Promo Model
**Location**: `app/Models/Promo.php`
**Constants**:
```php
DISCOUNT_PERCENT = 'percent'
DISCOUNT_FIXED = 'fixed'
STATUS_ACTIVE = 'active'
STATUS_INACTIVE = 'inactive'
```
**Relations**:
- `rentalCompany()` - BelongsTo RentalCompany
- `bookings()` - HasMany Booking
**Accessor**:
- `discount_label` - Format display diskon (e.g., "50%" atau "Rp 100.000")
### Booking Model Update
**Added**:
- `promo()` - BelongsTo Promo (nullable)
- `promo_id` - Foreign key field (dalam fillable)
### RentalCompany Model Update
**Added**:
- `promos()` - HasMany Promo
- `bookings()` - HasMany Booking
---
## 4. Service Layer
### PromoService
**Location**: `app/Services/PromoService.php`
**Key Methods**:
#### `getVisiblePromosForBooking(string $promoCode, int $rentalCompanyId, int $customerId, float $subtotal, bool $lockForUpdate = false)`
- Filter promo yang bisa digunakan customer
- Return collection dengan attributes:
- `can_use` (boolean)
- `cannot_use_reason` (string, jika can_use = false)
- `estimated_discount` (float)
- Validasi: status active, dalam periode, belum expired, loyal_only check, quota check
#### `resolvePromoForBooking(string $promoCode, int $rentalCompanyId, int $customerId, float $subtotal, bool $lockForUpdate = false)`
- Strict validation dengan transaction lock jika `$lockForUpdate = true`
- Throw `ValidationException` jika invalid
- Return array: `['promo' => Promo, 'discount_amount' => float]`
#### `calculateDiscount(Promo $promo, float $subtotal)`
- Handle percent vs fixed discount
- Cap diskon tidak boleh melebihi subtotal
#### `isLoyalCustomer(int $customerId, int $rentalCompanyId)`
- Check apakah customer telah menyelesaikan ≥ 2 bookings di rental
- `LOYAL_COMPLETED_THRESHOLD = 2`
---
## 5. Request Validation Layer
### StorePromoRequest
**Location**: `app/Http/Requests/StorePromoRequest.php`
**Validasi**:
- `title`: required, string, max 255
- `promo_code`: required, alpha_dash, max 50, unique:promos (per rental_company)
- `description`: nullable, string
- `discount_type`: required, in:percent,fixed
- `discount_value`: required, numeric, > 0
- `min_transaction`: nullable, numeric, >= 0
- `start_date`: required, date_format:Y-m-d H:i, before:end_date
- `end_date`: required, date_format:Y-m-d H:i
- `quota`: nullable, integer, >= 1
- `loyal_only`: nullable, boolean
- `status`: required, in:active,inactive
### UpdatePromoRequest
- Sama seperti StorePromoRequest
- `promo_code` unique ignore current promo id
### StoreBookingRequest Update
- `promo_code`: nullable, string, max 50
- Validasi di controller melalui PromoService
---
## 6. Controller Layer
### PromoController
**Location**: `app/Http/Controllers/AdminRental/PromoController.php`
**Methods** (7 total):
1. `index()` - List dengan filter search & status, pagination 10 item
2. `create()` - Show form create
3. `store()` - Save promo baru, set rental_company_id dari logged-in admin
4. `edit()` - Show form edit
5. `update()` - Update promo (promo_code read-only)
6. `destroy()` - Soft/hard delete promo
7. `toggle()` - Toggle status active/inactive
**Access Control**:
- `getRentalCompanyOrAbort()` - Ensure admin punya rental company
- `ensurePromoBelongsToRental()` - Check promo milik rental admin
**Data Normalization**:
- Promo code otomatis uppercase saat save
### CustomerController
**Location**: `app/Http/Controllers/AdminRental/CustomerController.php`
**Methods** (2 public):
1. `index()` - List customer dengan aggregates dan filter loyal/non-loyal
2. `show()` - Detail customer dengan booking history, reviews, vehicle history
**Index Query Features**:
- `withCount(['bookings as booking_count', 'bookings as completed_booking_count'])`
- `withSum('bookings as total_transaction_amount', 'total_amount')`
- `withMax('bookings as last_booking_at', 'created_at')`
- `withAvg('reviews as average_rating_given', 'rating')`
- Filter loyal: `whereIn($loyalCustomerIdSubQuery)` via robust subquery
- Filter non-loyal: `whereNotIn($loyalCustomerIdSubQuery)`
- Search: name & email
- Pagination: 10 item per halaman
**Show Query Features**:
- Bookings: eager load vehicle, payment, review, promo (paginated 8 per halaman)
- Reviews: all reviews customer di rental (sorted latest)
- Vehicles: distinct list of vehicles customer pernah rental
- Stats: bookingCount, completedCount, totalTransactionAmount, lastBookingDate
---
## 7. Route Registration
**Location**: `routes/web.php`
### Promo Routes (7 routes)
```
GET /admin-rental/promos → PromoController@index (admin-rental.promos.index)
GET /admin-rental/promos/create → PromoController@create (admin-rental.promos.create)
POST /admin-rental/promos → PromoController@store (admin-rental.promos.store)
GET /admin-rental/promos/{promo}/edit → PromoController@edit (admin-rental.promos.edit)
PUT /admin-rental/promos/{promo} → PromoController@update (admin-rental.promos.update)
DELETE /admin-rental/promos/{promo} → PromoController@destroy (admin-rental.promos.destroy)
PATCH /admin-rental/promos/{promo}/toggle → PromoController@toggle (admin-rental.promos.toggle)
```
### Customer CRM Routes (2 routes)
```
GET /admin-rental/customers → CustomerController@index (admin-rental.customers.index)
GET /admin-rental/customers/{customer} → CustomerController@show (admin-rental.customers.show)
```
### Booking Integration
- StoreBookingRequest validation include promo_code
- BookingController@store inject PromoService
- In transaction: call resolvePromoForBooking with lock
- Increment promo.used_count saat booking saved
- Handle ValidationException → return ke form with errors
---
## 8. View Layer
### Admin Promo CRUD (4 files)
#### `resources/views/admin-rental/promos/index.blade.php`
- Table dengan kolom: code, title, discount type/value, min transaction, kuota, terpakai, periode, loyal only, status, aksi
- Search by title & promo code
- Filter by status (active/inactive)
- Pagination dengan withQueryString()
- Edit & Delete buttons per row
#### `resources/views/admin-rental/promos/form.blade.php`
- Reusable form untuk create & edit
- Fields: title, promo_code (read-only saat edit), discount_type, discount_value, min_transaction, start_date, end_date, kuota, status, description, loyal_only checkbox
- Validasi display errors from Form Request
- Submit button text dinamis (Simpan/Update)
#### `resources/views/admin-rental/promos/create.blade.php`
- Wrapper page untuk create
- Include form.blade.php dengan action POST ke store
#### `resources/views/admin-rental/promos/edit.blade.php`
- Wrapper page untuk edit
- Include form.blade.php dengan action PUT ke update
---
### Admin CRM Customer (2 files)
#### `resources/views/admin-rental/customers/index.blade.php`
- Table dengan kolom: nama, email, HP, total booking, booking selesai, total transaksi, rata-rata rating, booking terakhir, status loyalitas, aksi
- Search by name & email
- Filter: Semua Customer, Loyal (2+ booking), Non-Loyal (<2 booking)
- Show badge 🏆 untuk loyal customer
- Pagination
- Detail button link ke show page
#### `resources/views/admin-rental/customers/show.blade.php`
- 4-col stat cards: total booking, completed booking, total transaction, average rating
- 2-column layout:
- **Left**: Data pribadi, riwayat booking (10 terbaru paginated), review yang diberikan
- **Right**: Informasi loyalitas (status + syarat), kendaraan yang pernah dirental, rekomendasi target promo
- Booking history: code (link to detail), vehicle, tanggal, total, status
- Reviews: vehicle, booking code, rating (⭐), review text, waktu
- Vehicles: nama, brand, category (link to edit)
---
### Booking Integration (2 files updated)
#### `resources/views/booking/promo-voucher.blade.php`
- Input untuk promo_code (text field, uppercase)
- List available promos dari controller dengan:
- Kode promo, judul, discount label
- Tombol "Pakai" jika promo dapat digunakan
- Pesan error jika tidak dapat digunakan (quota, expiry, etc)
- Kuota status (X dari Y tersisa)
- Auto-fill promo code saat click "Pakai" button
- Info text: "Diskon dihitung 100% di backend"
#### `resources/views/booking/ringkasan-biaya.blade.php`
- Updated calculation untuk menampilkan estimated discount
- Jika promo_code ada di request, estimasi diskon ditampilkan
- Diskon row highlight green jika ada
- Info text: "Nilai estimasi, final dihitung di backend"
---
### Sidebar Update
**Location**: `resources/views/components/admin-rental-sidebar.blade.php`
- Updated link "Data Customer" `route('admin-rental.customers.index')`
- Updated link "Promo" `route('admin-rental.promos.index')`
- Active state check untuk kedua menu
---
## 9. Booking Flow Integration
### Customer Booking Create Page
**Flow**:
1. BookingController@create:
- Get `$availablePromos` via `$this->promoService->getVisiblePromosForBooking()`
- Pass ke view untuk render daftar promo yang bisa dipakai
2. User input promo code ringkasan-biaya.blade.php kalkulasi estimasi diskon
3. Form submit BookingController@store:
- Validasi promo_code via StoreBookingRequest
- Call `$this->promoService->resolvePromoForBooking()` dalam DB transaction dengan lockForUpdate
- If valid set `booking.promo_id` dan hitung `booking.discount_amount`
- Increment `promo.used_count`
- If ValidationException return back with errors
---
## 10. Key Implementation Details
### Promo Code Normalization
- Automatic uppercase saat save
- Display uppercase di UI
### Validation di Backend
- StorePromoRequest: validasi format & unique per rental
- resolvePromoForBooking: validasi saat booking dengan lock untuk prevent race condition
- PromoService: bisnis logic terpusat & reusable
### Loyalitas Filter Approach
```php
// Robust subquery untuk reliable distinct count
$loyalCustomerIdSubQuery = Booking::query()
->select('customer_id')
->where('rental_company_id', $rentalCompanyId)
->where('booking_status', Booking::BOOKING_COMPLETED)
->groupBy('customer_id')
->havingRaw('COUNT(*) >= ?', [2]);
// Gunakan whereIn/whereNotIn (lebih reliable dari having)
$customers->whereIn('id', $loyalCustomerIdSubQuery) // loyal
$customers->whereNotIn('id', $loyalCustomerIdSubQuery) // non-loyal
```
### Transaction Locking
```php
DB::transaction(function () {
$promo = Promo::where('id', $promoId)->lockForUpdate()->first();
// Validate promo (status, dates, quota)
$promo->increment('used_count');
// Save booking dengan promo_id
});
```
---
## 11. Testing Checklist
### Promo Admin Module
- [ ] Create promo (valid & invalid input)
- [ ] Edit promo (cannot change promo_code)
- [ ] Delete promo
- [ ] Toggle status active/inactive
- [ ] Search by title & promo code
- [ ] Filter by status
- [ ] Pagination works
- [ ] Only see own rental's promos
- [ ] Promo code normalized uppercase
### CRM Customer Module
- [ ] List customers dengan aggregates correct
- [ ] Filter loyal (2+ completed bookings)
- [ ] Filter non-loyal
- [ ] Search by name & email
- [ ] View customer detail
- [ ] Booking history paginated correctly
- [ ] Reviews displayed correct
- [ ] Vehicles list correct
- [ ] Loyalty status calculated correct
- [ ] Only see own rental's customers
### Booking Integration
- [ ] See available promos list di create page
- [ ] List shows correct promo details & estimated discount
- [ ] Click "Pakai" auto-fill promo code
- [ ] Invalid promo shows error reason
- [ ] Expired promo shows "tidak dapat digunakan"
- [ ] Loyal-only promo filtered correctly
- [ ] Quota checks work
- [ ] ringkasan-biaya shows estimated discount
- [ ] Backend recalculate final discount saat save
- [ ] promo.used_count increment saat booking saved
- [ ] Promo validation dengan lock prevent race condition
---
## 12. File Manifest
### Created Files (15)
1. `database/migrations/2026_04_21_000011_create_promos_table.php`
2. `database/migrations/2026_04_21_000012_add_promo_foreign_key_to_bookings_table.php`
3. `app/Models/Promo.php`
4. `app/Services/PromoService.php`
5. `app/Http/Requests/StorePromoRequest.php`
6. `app/Http/Requests/UpdatePromoRequest.php`
7. `app/Http/Controllers/AdminRental/PromoController.php`
8. `app/Http/Controllers/AdminRental/CustomerController.php`
9. `resources/views/admin-rental/promos/index.blade.php`
10. `resources/views/admin-rental/promos/form.blade.php`
11. `resources/views/admin-rental/promos/create.blade.php`
12. `resources/views/admin-rental/promos/edit.blade.php`
13. `resources/views/admin-rental/customers/index.blade.php`
14. `resources/views/admin-rental/customers/show.blade.php`
15. `resources/views/booking/promo-voucher.blade.php` (updated)
### Modified Files (8)
1. `app/Models/Booking.php` - Added promo() relation
2. `app/Models/RentalCompany.php` - Added promos() & bookings() relations
3. `app/Http/Requests/StoreBookingRequest.php` - Added promo_code validation
4. `app/Http/Controllers/Customer/BookingController.php` - Integrated PromoService
5. `routes/web.php` - Registered promo & customer routes
6. `resources/views/components/admin-rental-sidebar.blade.php` - Updated menu links
7. `resources/views/booking/ringkasan-biaya.blade.php` - Display estimated discount
8. `app/Models/User.php` - Removed invalid promos() relation (fixed)
---
## 13. Next Steps (Out of Scope for Tahap 8)
- [ ] Run migration di database real
- [ ] Integration testing booking promo flow end-to-end
- [ ] Super admin module untuk approve/manage all promos
- [ ] Email notification untuk customer dapat promo baru
- [ ] Advanced CRM: email campaign, promo targeting automation
- [ ] Analytics dashboard untuk promo performance
---
## 14. Notes
- Semua promo validation 100% di backend, frontend hanya estimasi
- Admin rental access control enforce di setiap controller action
- Loyalitas logic konsisten di promo filter & CRM display
- Service pattern keep business logic terpusat & testable
- Blade templates follow existing admin rental pattern untuk consistency