Delete DOKUMENTASI_TAHAP8_BACKEND_PROMO_CRM.md
This commit is contained in:
parent
9e4afab8b6
commit
78d8392254
|
|
@ -1,446 +0,0 @@
|
||||||
# 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
|
|
||||||
Loading…
Reference in New Issue