MIF_E31232103/TAHAP_9_DOKUMENTASI.md

18 KiB

Tahap 9: Fondasi Infrastruktur Sistem Rental

Status: COMPLETED
Tanggal: Fase 1 - Audit & Discovery
Objective: Membangun fondasi teknis untuk sistem yang stabil, aman, dan scalable


📋 Daftar Isi

  1. Overview
  2. Deliverables
  3. Models & Database
  4. Services Layer
  5. Controllers Refactoring
  6. Implementation Details
  7. Technical Architecture

Overview

Tahap 9 adalah fase foundational yang membangun infrastruktur inti untuk mendukung Tahap 10 (hardening). Fokus utama:

  • Membuat service layer untuk business logic
  • Implementing policies untuk authorization
  • Mengintegrasikan file upload dengan validation
  • Slug management system yang konsisten
  • Migration & model standardization

Scope Tahap 9

TIDAK menambah fitur bisnis besar, hanya:

  • Infrastructure improvements
  • Code organization & patterns
  • Security foundation
  • Data integrity

Deliverables

1. Service Layer (4 Services)

PromoService (Existing)

// Location: app/Services/PromoService.php
// Purpose: Promo business logic
const LOYAL_COMPLETED_THRESHOLD = 3;

public function isUserLoyal($userId, $rentalCompanyId): bool
public function getApplicablePromos($bookingData): Collection

SlugService (New)

// Location: app/Services/SlugService.php
// Purpose: Centralized slug generation dengan uniqueness guarantee

public function generateUnique(
    string $modelClass, 
    string $column, 
    string $source, 
    ?int $ignoreId = null
): string

Fitur:

  • Automatic slug generation dari source text
  • Collision detection & appending (-1, -2, etc)
  • Support untuk model yang berbeda
  • Case-insensitive duplicate checking

FileUploadService (New)

// Location: app/Services/FileUploadService.php
// Purpose: Secure file upload dengan naming & organization

public function storePublic(
    UploadedFile $file, 
    string $directory
): string // returns "vehicles/main/2026/04/uuid.jpg"

public function deletePublic(string $path): bool

Fitur:

  • UUID-based filename (prevents collisions)
  • Date-based subdirectories (Y/m format)
  • Safe deletion with error handling
  • Returns relative path untuk storage

NotificationService (New - Tahap 10 prep)

// Location: app/Services/NotificationService.php
// Purpose: Database notification management

public function notifyUser(
    int $userId, 
    string $title, 
    string $message, 
    string $type,
    ?string $url = null,
    ?string $referenceType = null,
    ?int $referenceId = null
): UserNotification

ActivityLogService (New - Tahap 10 prep)

// Location: app/Services/ActivityLogService.php
// Purpose: Immutable audit logging

public function log(
    string $action,
    string $description,
    string $targetType,
    int $targetId,
    ?array $meta = null,
    ?int $userId = null
): ActivityLog

2. Models & Database Migrations

Models Created/Updated

UserNotification

// Table: user_notifications
// Columns:
// - id: bigint
// - user_id: bigint (FK users)
// - title: string
// - message: text
// - type: enum (info, success, warning, error)
// - reference_type: string (nullable) - booking, payment, etc
// - reference_id: bigint (nullable)
// - url: string (nullable)
// - read_at: timestamp (nullable)
// - created_at, updated_at

// Key Methods:
public function markAsRead(): self
public function isUnread(): bool

// Relationships:
public function user(): BelongsTo

ActivityLog

// Table: activity_logs
// Columns:
// - id: bigint
// - user_id: bigint (FK users, nullable for system actions)
// - action: string (e.g., 'vehicle.created')
// - target_type: string (vehicle, booking, payment, etc)
// - target_id: bigint
// - description: text
// - meta: json (contextual data)
// - created_at (no updated_at - append-only)

// Indexes:
// - user_id
// - action
// - created_at

// Relationships:
public function user(): BelongsTo

RentalCompany (Enhanced)

  • Booted with automatic company_slug generation
  • Uses SlugService::generateUnique()

Vehicle (Enhanced)

  • Uses SlugService in controller for slug generation
  • Primary image relationship optimized

Migration Files

Migrations/2026_04_23_xxxxxx_create_user_notifications_table.php

Schema::create('user_notifications', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
    $table->string('title');
    $table->text('message');
    $table->enum('type', ['info', 'success', 'warning', 'error']);
    $table->string('reference_type')->nullable();
    $table->unsignedBigInteger('reference_id')->nullable();
    $table->string('url')->nullable();
    $table->timestamp('read_at')->nullable();
    $table->timestamps();
    
    $table->index(['user_id', 'read_at']);
    $table->index('created_at');
});

Migrations/2026_04_23_xxxxxx_create_activity_logs_table.php

Schema::create('activity_logs', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->nullable()->constrained('users')->cascadeOnDelete();
    $table->string('action');
    $table->string('target_type');
    $table->unsignedBigInteger('target_id');
    $table->text('description');
    $table->json('meta')->nullable();
    $table->timestamp('created_at')->useCurrent();
    
    $table->index('user_id');
    $table->index('action');
    $table->index('created_at');
    $table->index(['target_type', 'target_id']);
});

3. Policies Layer

6 Policy Classes

VehiclePolicy

// Location: app/Policies/VehiclePolicy.php
public function view(User $user, Vehicle $vehicle): bool
    // admin_rental owns rental_company OR super_admin

public function create(User $user): bool
    // admin_rental must have rental_company

public function update(User $user, Vehicle $vehicle): bool
    // Delegates to view()

public function delete(User $user, Vehicle $vehicle): bool
    // Delegates to view()

BookingPolicy

// Location: app/Policies/BookingPolicy.php
public function view(User $user, Booking $booking): bool
    // customer owns booking OR admin_rental owns rental_company OR super_admin

public function create(User $user): bool
    // customer only

public function update(User $user, Booking $booking): bool
    // Delegates to view()

PaymentPolicy

// Location: app/Policies/PaymentPolicy.php
public function view(User $user, Payment $payment): bool
    // customer owns booking OR admin_rental owns rental_company OR super_admin

public function update(User $user, Payment $payment): bool
    // Delegates to view()

PromoPolicy

// Location: app/Policies/PromoPolicy.php
public function view(User $user, Promo $promo): bool
    // admin_rental owns rental_company OR super_admin

public function create(User $user): bool
    // admin_rental must have rental_company

public function update(User $user, Promo $promo): bool
    // Delegates to view()

public function delete(User $user, Promo $promo): bool
    // Delegates to view()

ReviewPolicy

// Location: app/Policies/ReviewPolicy.php
public function view(User $user, Review $review): bool
    // customer/admin_rental/super_admin with access

public function create(User $user, Booking $booking): bool
    // customer owns completed booking with no existing review

RentalCompanyPolicy

// Location: app/Policies/RentalCompanyPolicy.php
public function view(User $user, RentalCompany $company): bool
    // admin_rental owner OR super_admin

public function verify(User $user): bool
    // super_admin only

public function update(User $user, RentalCompany $company): bool
    // Delegates to view()

Registration di AppServiceProvider

// Location: app/Providers/AppServiceProvider.php
protected $policies = [
    Vehicle::class => VehiclePolicy::class,
    Booking::class => BookingPolicy::class,
    Payment::class => PaymentPolicy::class,
    Promo::class => PromoPolicy::class,
    Review::class => ReviewPolicy::class,
    RentalCompany::class => RentalCompanyPolicy::class,
];

4. Controllers Refactoring

AdminRental/VehicleController

Changes:

  • Added SlugService dependency injection
  • Added FileUploadService dependency injection
  • Added ActivityLogService integration (prep for Tahap 10)
  • All CRUD methods use $this->authorize() checks
  • File upload with try-catch error handling
  • Dated subdirectory organization for uploads

Methods:

public function __construct(
    private readonly SlugService $slugService,
    private readonly FileUploadService $fileUploadService,
    private readonly ActivityLogService $activityLogService,
) {}

public function index(): View
public function create(): View
public function store(StoreVehicleRequest $request): RedirectResponse
public function edit(Vehicle $vehicle): View
public function update(UpdateVehicleRequest $request, Vehicle $vehicle): RedirectResponse
public function destroy(Vehicle $vehicle): RedirectResponse
public function destroyGalleryImage(VehicleImage $image): RedirectResponse

Customer/BookingController

Changes:

  • Added authorization checks
  • NotificationService integration (Tahap 10)
  • ActivityLogService integration (Tahap 10)

Customer/PaymentController

Changes:

  • Added FileUploadService dependency
  • File upload validation tightened
  • MIME type & size checking
  • Safe file deletion on update
  • Authorization checks on all methods
  • Notification/Logging (Tahap 10)

Customer/ReviewController

Changes:

  • Policy-based authorization replacing manual checks
  • Service injection ready for Tahap 10

AdminRental/BookingController

Changes:

  • Authorization on show/update methods
  • Service integration ready

AdminRental/PaymentController

Changes:

  • Authorization checks on all methods
  • Service integration for verify/reject

AdminRental/PromoController

Changes:

  • Policies on all CRUD methods
  • Service integration ready

AdminRental/ReviewController

Changes:

  • Authorization check on index

AdminRental/CustomerController

Changes:

  • Authorization check on index

Customer/MyBookingController

Changes:

  • Policy-based authorization

5. Form Validation Hardening

Vehicle Validation (StoreVehicleRequest)

'thumbnail' => ['required', 'file', 'mimetypes:image/jpeg,image/png', 'max:4096'],
'gallery_images.*' => ['nullable', 'file', 'mimetypes:image/jpeg,image/png', 'max:4096'],

Payment Validation (UploadProofRequest)

'proof_file' => [
    'required',
    'file',
    'mimetypes:image/jpeg,image/png,application/pdf',
    'max:8192',
],

Promo Validation (StorePromoRequest)

'title' => ['required', 'string', 'max:100'],
'promo_code' => ['required', 'string', 'max:20', 'unique:promos'],
'discount_value' => ['required', 'numeric', 'min:0'],

Implementation Details

SlugService Usage

Di VehicleController:

$slug = $this->slugService->generateUnique(
    Vehicle::class,
    'slug',
    $validated['name']
);

$vehicle = Vehicle::create([
    ...$validated,
    'slug' => $slug,
]);

Di RentalCompany Model (Boot Event):

protected static function boot(): void
{
    parent::boot();
    
    static::creating(function (self $model): void {
        if (!$model->company_slug) {
            $slug = app(SlugService::class)->generateUnique(
                self::class,
                'company_slug',
                $model->company_name
            );
            $model->company_slug = $slug;
        }
    });
}

FileUploadService Usage

Store File:

try {
    $path = $this->fileUploadService->storePublic(
        $request->file('thumbnail'),
        'vehicles/main'
    );
    // Path: "vehicles/main/2026/04/uuid.jpg"
} catch (\Throwable $exception) {
    return back()->withInput()->with('error', 'Upload gagal');
}

Delete File:

$this->fileUploadService->deletePublic($vehicle->thumbnail);

Authorization Usage

Via Policy:

// Single action
$this->authorize('view', $vehicle);

// Class-level
$this->authorize('create', Vehicle::class);

// Custom gate
$this->authorize('verify', $rentalCompany);

Technical Architecture

Folder Structure (After Tahap 9)

app/
├── Services/
│   ├── PromoService.php           (Existing)
│   ├── SlugService.php            (New)
│   ├── FileUploadService.php      (New)
│   ├── NotificationService.php    (New - Tahap 10 prep)
│   └── ActivityLogService.php     (New - Tahap 10 prep)
├── Policies/
│   ├── VehiclePolicy.php
│   ├── BookingPolicy.php
│   ├── PaymentPolicy.php
│   ├── PromoPolicy.php
│   ├── ReviewPolicy.php
│   └── RentalCompanyPolicy.php
├── Models/
│   ├── User.php (Updated)
│   ├── Vehicle.php (Updated)
│   ├── Booking.php (Updated)
│   ├── Payment.php (Updated)
│   ├── Promo.php (Updated)
│   ├── Review.php (Updated)
│   ├── RentalCompany.php (Updated)
│   ├── UserNotification.php       (New)
│   └── ActivityLog.php            (New)
├── Http/
│   ├── Requests/ (Validation tightened)
│   └── Controllers/ (Refactored dengan service injection)
└── Providers/
    └── AppServiceProvider.php     (Policies registered)

database/
├── migrations/
│   ├── 2026_04_23_xxxxxx_create_user_notifications_table.php
│   └── 2026_04_23_xxxxxx_create_activity_logs_table.php
└── seeders/ (Unchanged)

Design Patterns

1. Service Layer Pattern

Business logic abstracted from controllers:

Controller → Service → Model → Database

Benefits:

  • Reusability across controllers
  • Testability
  • Single responsibility
  • Easy to mock

2. Policy Pattern

Authorization logic centralized:

$this->authorize('action', Model) → Policy → User Role Check

Benefits:

  • Consistent authorization
  • Maintainable permissions
  • Testable policies

3. Dependency Injection

Constructor injection untuk semua services:

public function __construct(
    private readonly ServiceA $serviceA,
    private readonly ServiceB $serviceB,
) {}

4. Immutable Logging

ActivityLog append-only (no updates):

Created → Never Updated → Audit Trail Safe

Database Relationships

Users (1) ──→ (Many) UserNotifications
      (1) ──→ (Many) ActivityLogs
      (1) ──→ (1) RentalCompany
      (1) ──→ (Many) Bookings

RentalCompany (1) ──→ (Many) Vehicles
              (1) ──→ (Many) Promos
              (1) ──→ (Many) Bookings

Bookings (1) ──→ (1) Payment
       (1) ──→ (1) Review
       (1) ──→ (1) Vehicle

Vehicles (1) ──→ (Many) VehicleImages
       (1) ──→ (Many) Bookings

Validation Rules Summary

Vehicle Upload

  • Format: JPEG, PNG
  • Size: Max 4MB per image
  • Required: Thumbnail
  • Optional: Gallery (multiple)

Payment Upload

  • Format: JPEG, PNG, PDF
  • Size: Max 8MB
  • Required: File type check
  • Transaction: Atomic with DB update

Promo Creation

  • Code: Unique, uppercase, max 20 chars
  • Title: Required, max 100 chars
  • Discount: Numeric, non-negative
  • Dates: End date >= Start date

Error Handling

FileUploadService

try {
    $path = $fileUploadService->storePublic($file, $dir);
} catch (\Throwable $e) {
    // User-friendly error message
    return back()->with('error', 'Upload failed');
}

Authorization

try {
    $this->authorize('action', $model);
} catch (AuthorizationException $e) {
    // Laravel handles: 403 Forbidden response
}

Validation

// FormRequest handles automatically
// Returns back with errors on validation fail

Performance Considerations

  1. Slug Generation: Single query per unique check (optimized)
  2. File Upload: UUID prevents lookups, dated dirs enable cleanup
  3. Activity Logs: Append-only (no locks), indexes on common queries
  4. Policies: Eager loading relationships to avoid N+1
  5. Queries: withCount, withSum untuk statistics tanpa extra queries

Testing Checklist

  • SlugService generates unique slugs
  • SlugService handles collisions
  • FileUploadService stores with UUID naming
  • FileUploadService deletes safely
  • Policies correctly authorize actions
  • Services inject correctly in controllers
  • File validation rejects invalid types
  • File validation enforces size limits
  • RentalCompany auto-generates slug
  • Activity logs capture correctly (Tahap 10)
  • Notifications send correctly (Tahap 10)

Tahap 9 Summary

COMPLETED:

  • 4 Services (Promo, Slug, FileUpload, Notification/ActivityLog prep)
  • 6 Policies with consistent pattern
  • 8 Controllers refactored with service injection
  • 2 New models with migrations
  • Validation hardening
  • Authorization layer implementation

NEXT (Tahap 10):

  • Notification integration (8 triggers)
  • Activity logging integration (15+ actions)
  • Error pages & components
  • Report filtering enhancements
  • UI/UX polish

Quick Reference

Service Injection

use App\Services\SlugService;
use App\Services\FileUploadService;

public function __construct(
    private readonly SlugService $slugService,
    private readonly FileUploadService $fileUploadService,
) {}

Authorization Check

$this->authorize('action', $model);
$this->authorize('create', ModelClass::class);

Slug Generation

$slug = $this->slugService->generateUnique(
    Vehicle::class, 'slug', $sourceName
);

File Upload

$path = $this->fileUploadService->storePublic(
    $request->file('field'), 'directory'
);

Dokumentasi Tahap 9 Complete
Untuk Tahap 10, lihat: TAHAP_10_DOKUMENTASI.md