# 📘 Architecture Guide Book
## Laravel Modular Monolith + Full DDD + React Inertia

> **Panduan ini adalah pedoman untuk AI Agent dan developer** dalam membangun dan me-refactor project Laravel menggunakan arsitektur **Modular Monolith** dengan **Full DDD (Domain-Driven Design)** dan **CQRS**.

---

## Daftar Isi

1. [Prinsip Arsitektur](#1-prinsip-arsitektur)
2. [Struktur Folder](#2-struktur-folder)
3. [Layer Rules & Dependency](#3-layer-rules--dependency)
4. [Domain Layer](#4-domain-layer)
5. [Application Layer](#5-application-layer)
6. [Infrastructure Layer](#6-infrastructure-layer)
7. [Frontend (React + Inertia)](#7-frontend-react--inertia)
8. [Naming Convention](#8-naming-convention)
9. [Alur Kerja: Membuat Fitur Baru](#9-alur-kerja-membuat-fitur-baru)
10. [Komunikasi Antar Module](#10-komunikasi-antar-module)
11. [Testing Strategy](#11-testing-strategy)
12. [Panduan Refactoring](#12-panduan-refactoring)
13. [Anti-Pattern (Yang Harus Dihindari)](#13-anti-pattern-yang-harus-dihindari)
14. [Checklist Review Code](#14-checklist-review-code)
15. [Audit Trail](#15-audit-trail)
16. [Permission & Authorization](#16-permission--authorization)
17. [State Machine & Workflow](#17-state-machine--workflow)
18. [Error Handling Strategy](#18-error-handling-strategy)
19. [Database Conventions](#19-database-conventions)
20. [Recommended Packages](#20-recommended-packages)
21. [Git & Version Control](#21-git--version-control)
22. [Environment & Configuration](#22-environment--configuration)
23. [Code Style & Formatting](#23-code-style--formatting)
24. [Security Checklist](#24-security-checklist)
25. [Multi-App Platform & SSO](#25-multi-app-platform--sso)
26. [Deployment & Infrastructure](#26-deployment--infrastructure)
27. [Pagination Profiles (Cross App Standard)](#27-pagination-profiles-cross-app-standard)

---

## 1. Prinsip Arsitektur

### 1.1 Modular Monolith
- Aplikasi adalah **satu codebase** (monolith) yang terbagi menjadi **module-module independen**
- Setiap module memiliki **bounded context** sendiri: domain bisnis yang jelas batasannya
- Module berkomunikasi melalui **event** atau **shared contracts**, BUKAN direct import model

### 1.2 Full DDD (3 Layer)
Setiap module memiliki 3 layer:

| Layer | Tanggung Jawab | Boleh Import |
|---|---|---|
| **Domain** | Aturan bisnis murni | Tidak boleh import dari layer lain |
| **Application** | Alur kerja / use case | Hanya boleh import dari Domain |
| **Infrastructure** | Framework, HTTP, database | Boleh import dari semua layer |

### 1.3 CQRS (Command Query Responsibility Segregation)
- **Command** (tulis) = `Action` classes → mengubah state (create, update, delete)
- **Query** (baca) = `Query` classes → membaca data tanpa mengubah state

---

## 2. Struktur Folder

### 2.1 Struktur Per Module

```
app/Modules/{ModuleName}/
│
├── Domain/                              💎 ATURAN BISNIS
│   ├── Models/                          ← Eloquent models (entity)
│   │   └── {Model}.php
│   ├── Enums/                           ← Status, tipe, kategori
│   │   └── {Model}{Field}.php
│   ├── Events/                          ← Domain events
│   │   └── {Model}{Action}.php
│   ├── Contracts/                       ← Interfaces / port
│   │   └── {Model}RepositoryInterface.php
│   ├── Exceptions/                      ← Domain-specific errors
│   │   └── {Deskripsi}Exception.php
│   ├── Services/                        ← Business rules lintas entity
│   │   └── {Deskripsi}Service.php
│   └── ValueObjects/                    ← Immutable objects
│       └── {Name}.php
│
├── Application/                         🎯 USE CASE / ALUR KERJA
│   ├── Actions/                         ← Command (write operations)
│   │   └── {Verb}{Model}Action.php
│   ├── Queries/                         ← Query (read operations)
│   │   └── Get{Model}{Filter}Query.php
│   ├── DTOs/                            ← Data Transfer Objects
│   │   └── {Verb}{Model}DTO.php
│   ├── Listeners/                       ← React to domain events
│   │   └── {Action}Listener.php
│   └── Jobs/                            ← Background tasks
│       └── {Verb}{Model}Job.php
│
├── Infrastructure/                      🔧 TEKNIS / FRAMEWORK
│   ├── Controllers/                     ← HTTP endpoints
│   │   └── {Model}Controller.php
│   ├── Requests/                        ← Form validation
│   │   └── {Verb}{Model}Request.php
│   ├── Repositories/                    ← Database implementations
│   │   └── Eloquent{Model}Repository.php
│   ├── Policies/                        ← Authorization
│   │   └── {Model}Policy.php
│   ├── Routes/                          ← Route definitions
│   │   ├── web.php
│   │   └── api.php
│   ├── Providers/                       ← Service provider
│   │   └── {Module}ServiceProvider.php
│   ├── Resources/                       ← API Resources (opsional)
│   │   └── {Model}Resource.php
│   └── Exports/                         ← Export PDF/Excel (opsional)
│       └── {Model}Export.php
│
└── Tests/
    ├── Unit/                            ← Test Domain & Application
    └── Feature/                         ← Test Infrastructure (HTTP)
```

### 2.2 Shared Module

```
app/Modules/Shared/
├── Domain/
│   ├── Contracts/                       ← RepositoryInterface, HasUuid
│   └── ValueObjects/                    ← Email, Money, dsb
├── Application/
│   ├── Actions/BaseAction.php
│   └── Contracts/QueryInterface.php
└── Infrastructure/
    ├── Traits/                          ← HasUuidTrait, Auditable
    └── Helpers/                         ← ResponseHelper, dsb
```

### 2.3 Frontend

```
resources/js/
├── Pages/{ModuleName}/                  ← 1 folder per module
│   ├── Index.tsx
│   ├── Create.tsx
│   ├── Edit.tsx
│   └── Show.tsx
├── Components/
│   ├── UI/                              ← Button, Modal, Card, Badge
│   ├── Form/                            ← Input, Select, Datepicker
│   └── Table/                           ← DataTable, Pagination
├── Hooks/                               ← Custom React hooks
├── Utils/                               ← Helper functions
└── Layouts/                             ← AuthenticatedLayout, GuestLayout
```

---

## 3. Layer Rules & Dependency

```
┌─────────────────────────────────────────────────────────────┐
│                     Infrastructure                          │
│  Controllers, Requests, Repositories, Routes, Providers     │
│                         │                                   │
│                         ▼                                   │
│  ┌─────────────────────────────────────────────────────┐    │
│  │                  Application                         │    │
│  │  Actions, Queries, DTOs, Listeners, Jobs             │    │
│  │                         │                            │    │
│  │                         ▼                            │    │
│  │  ┌─────────────────────────────────────────────┐    │    │
│  │  │                 Domain                       │    │    │
│  │  │  Models, Enums, Events, Services, Contracts  │    │    │
│  │  └─────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
```

### Rules (WAJIB dipatuhi):

| Rule | Penjelasan |
|---|---|
| 🔴 Domain TIDAK BOLEH import Application | `Domain/Models/User.php` tidak boleh `use App\...\Actions\...` |
| 🔴 Domain TIDAK BOLEH import Infrastructure | `Domain/Services/...` tidak boleh `use App\...\Controllers\...` |
| 🔴 Application TIDAK BOLEH import Infrastructure | `Actions/...` tidak boleh `use App\...\Controllers\...` |
| 🟢 Infrastructure BOLEH import Application | Controller boleh panggil Action dan Query |
| 🟢 Infrastructure BOLEH import Domain | Controller boleh akses Model |
| 🟢 Application BOLEH import Domain | Action boleh akses Model, Event, Service |

---

## 4. Domain Layer

### 4.1 Models
```php
// ✅ BENAR: Model berisi domain logic
class Sample extends Model
{
    protected $casts = [
        'status' => SampleStatus::class, // Cast ke Enum
    ];

    // Domain logic
    public function isExpired(): bool
    {
        return $this->expired_at < now();
    }

    public function canBeEdited(): bool
    {
        return $this->status === SampleStatus::Draft;
    }

    // Relationships
    public function testResults(): HasMany
    {
        return $this->hasMany(TestResult::class);
    }
}
```

```php
// ❌ SALAH: Jangan taruh HTTP logic di Model
class Sample extends Model
{
    public function sendNotification() { ... }  // ← Ini harusnya di Listener
    public function exportToPdf() { ... }       // ← Ini harusnya di Infrastructure
}
```

### 4.2 Enums
```php
// Gunakan PHP 8.1+ backed enum
enum SampleStatus: string
{
    case Draft = 'draft';
    case Submitted = 'submitted';
    case InTesting = 'in_testing';
    case Reviewed = 'reviewed';
    case Approved = 'approved';
    case Rejected = 'rejected';
    case Released = 'released';

    public function label(): string
    {
        return match ($this) {
            self::Draft => 'Draft',
            self::InTesting => 'In Testing',
            // ... etc
        };
    }

    public function color(): string
    {
        return match ($this) {
            self::Draft => 'gray',
            self::Approved => 'green',
            self::Rejected => 'red',
            // ... etc
        };
    }
}
```

### 4.3 Events
```php
// Nama: {Model}{PastTenseVerb}
// Contoh: SampleCreated, OrderApproved, TestCompleted
class SampleApproved
{
    public function __construct(
        public readonly int $sampleId,
        public readonly int $approvedBy,
        public readonly \DateTimeImmutable $approvedAt,
    ) {}
}
```

### 4.4 Domain Services
```php
// Gunakan ketika:
// 1. Logic melibatkan >1 model
// 2. Bukan CRUD biasa
// 3. Aturan bisnis yang bisa di-reuse

class SampleAcceptanceCriteria
{
    /**
     * Cek apakah hasil test sample memenuhi spesifikasi.
     */
    public function evaluate(Sample $sample, Specification $spec): AcceptanceResult
    {
        $results = $sample->testResults;

        foreach ($results as $result) {
            $limit = $spec->getLimitFor($result->parameter);

            if (!$limit->contains($result->value)) {
                return AcceptanceResult::outOfSpec($result, $limit);
            }
        }

        return AcceptanceResult::passed();
    }
}
```

### 4.5 Contracts (Interfaces)
```php
// Definisikan interface di Domain, implementasi di Infrastructure
// Ini memungkinkan swap implementasi tanpa mengubah domain logic

interface SampleRepositoryInterface
{
    public function findById(int $id): ?Sample;
    public function findByBatchNumber(string $batchNumber): ?Sample;
    public function findPendingApproval(): Collection;
}
```

### 4.6 Exceptions
```php
// Domain exceptions — error yang terkait aturan bisnis
class SampleAlreadyApprovedException extends \DomainException
{
    public function __construct(Sample $sample)
    {
        parent::__construct(
            "Sample #{$sample->id} has already been approved and cannot be modified."
        );
    }
}
```

### 4.7 Value Objects
```php
// Immutable, no identity, equality by value
class ConcentrationUnit
{
    public function __construct(
        public readonly float $value,
        public readonly string $unit, // ppm, ppb, mg/L, etc.
    ) {}

    public function convertTo(string $targetUnit): self
    {
        // conversion logic
    }

    public function isWithinRange(float $min, float $max): bool
    {
        return $this->value >= $min && $this->value <= $max;
    }
}
```

---

## 5. Application Layer

### 5.1 Actions (Command/Write)
```php
// Aturan:
// 1. Satu Action = satu use case
// 2. Nama: {Verb}{Model}Action
// 3. Method utama: execute()
// 4. Boleh panggil Domain Service
// 5. Boleh fire Event
// 6. TIDAK boleh return HTTP response

class ApproveSampleAction extends BaseAction
{
    public function __construct(
        private SampleAcceptanceCriteria $criteria,
    ) {}

    public function execute(Sample $sample, User $approver, ApproveSampleDTO $dto): Sample
    {
        // 1. Validasi domain rules
        if ($sample->status !== SampleStatus::Reviewed) {
            throw new InvalidSampleStateException($sample, 'approve');
        }

        // 2. Cek acceptance criteria (Domain Service)
        $result = $this->criteria->evaluate($sample, $dto->specification);
        if (!$result->passed) {
            throw new SampleOutOfSpecException($sample, $result);
        }

        // 3. Update state
        $sample->update([
            'status' => SampleStatus::Approved,
            'approved_by' => $approver->id,
            'approved_at' => now(),
            'approval_notes' => $dto->notes,
        ]);

        // 4. Fire domain event
        event(new SampleApproved(
            sampleId: $sample->id,
            approvedBy: $approver->id,
            approvedAt: now(),
        ));

        return $sample->refresh();
    }
}
```

### 5.2 Queries (CQRS Read)
```php
// Aturan:
// 1. Satu Query = satu read operation
// 2. Nama: Get{What}{Filter}Query
// 3. Method utama: get()
// 4. TIDAK boleh mengubah data
// 5. Boleh punya parameter (search, filter, sort, pagination)

class GetPendingSamplesQuery implements QueryInterface
{
    public function __construct(
        private readonly ?int $laboratoryId = null,
        private readonly ?string $priority = null,
        private readonly int $perPage = 20,
    ) {}

    public function get(): LengthAwarePaginator
    {
        return Sample::query()
            ->where('status', SampleStatus::Submitted)
            ->when($this->laboratoryId, fn($q, $id) => $q->where('laboratory_id', $id))
            ->when($this->priority, fn($q, $p) => $q->where('priority', $p))
            ->with(['submittedBy', 'testMethod'])
            ->orderByDesc('priority')
            ->orderBy('submitted_at')
            ->paginate($this->perPage);
    }
}
```

### 5.3 DTOs
```php
// Aturan:
// 1. Semua property readonly
// 2. fromArray() factory method untuk konversi dari request
// 3. toArray() untuk konversi ke data yang bisa disimpan
// 4. TIDAK boleh ada logic bisnis di sini

class ApproveSampleDTO
{
    public function __construct(
        public readonly string $notes,
        public readonly int $specificationId,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            notes: $data['notes'],
            specificationId: $data['specification_id'],
        );
    }
}
```

### 5.4 Listeners
```php
// Nama: {DeskripsiAksi}Listener atau {DeskripsiAksi}OnEvent
// Listener di-register di EventServiceProvider atau module ServiceProvider

class GenerateCertificateOfAnalysis
{
    public function handle(SampleApproved $event): void
    {
        $sample = Sample::findOrFail($event->sampleId);
        // Generate CoA PDF...
    }
}
```

### 5.5 Jobs
```php
// Untuk background tasks yang memakan waktu
// Nama: {Verb}{Model}Job

class GenerateMonthlyReportJob implements ShouldQueue
{
    public function __construct(
        public readonly int $laboratoryId,
        public readonly string $month,
    ) {}

    public function handle(): void
    {
        // Heavy processing...
    }
}
```

---

## 6. Infrastructure Layer

### 6.1 Controllers
```php
// Aturan:
// 1. Controller HARUS tipis (thin controller)
// 2. Hanya: terima request → panggil Action/Query → return response
// 3. TIDAK boleh ada business logic di controller
// 4. Query untuk READ, Action untuk WRITE (CQRS)

class SampleController extends Controller
{
    // READ → Query
    public function index(Request $request): Response
    {
        $samples = (new GetPendingSamplesQuery(
            laboratoryId: $request->integer('lab_id') ?: null,
            priority: $request->string('priority')->toString() ?: null,
        ))->get();

        return Inertia::render('Sample/Index', [
            'samples' => $samples,
            'filters' => $request->only(['lab_id', 'priority']),
        ]);
    }

    // WRITE → Action
    public function approve(
        ApproveSampleRequest $request,
        Sample $sample,
        ApproveSampleAction $action,
    ): RedirectResponse {
        $dto = ApproveSampleDTO::fromArray($request->validated());
        $action->execute($sample, $request->user(), $dto);

        return redirect()->route('samples.index')
            ->with('success', 'Sample approved.');
    }
}
```

### 6.2 Requests
```php
// Validasi input saja, BUKAN business logic
class ApproveSampleRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('approve', $this->route('sample'));
    }

    public function rules(): array
    {
        return [
            'notes' => ['required', 'string', 'max:1000'],
            'specification_id' => ['required', 'exists:specifications,id'],
        ];
    }
}
```

### 6.3 Repositories
```php
// Implementasi dari Domain/Contracts interface
// Bind di ServiceProvider: Interface → Implementation

class EloquentSampleRepository implements SampleRepositoryInterface
{
    public function __construct(protected Sample $model) {}

    public function findById(int $id): ?Sample
    {
        return $this->model->find($id);
    }

    public function findByBatchNumber(string $batchNumber): ?Sample
    {
        return $this->model->where('batch_number', $batchNumber)->first();
    }

    public function findPendingApproval(): Collection
    {
        return $this->model
            ->where('status', SampleStatus::Reviewed)
            ->orderBy('submitted_at')
            ->get();
    }
}
```

### 6.4 ServiceProvider
```php
// Setiap module WAJIB punya ServiceProvider
// Isinya: routes, policies, bindings, event listeners

class SampleServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Bind interface → implementation
        $this->app->bind(
            SampleRepositoryInterface::class,
            EloquentSampleRepository::class,
        );
    }

    public function boot(): void
    {
        $this->registerRoutes();
        $this->registerPolicies();
    }

    protected function registerRoutes(): void
    {
        Route::middleware('web')
            ->group(module_path('Sample', 'Infrastructure/Routes/web.php'));
    }

    protected function registerPolicies(): void
    {
        Gate::policy(Sample::class, SamplePolicy::class);
    }
}
```

---

## 7. Frontend (React + Inertia)

### 7.1 Struktur Page
```tsx
// Nama file = nama route method (Index, Create, Edit, Show)
// Props di-type dengan interface TypeScript

interface Sample {
    id: number;
    batch_number: string;
    status: string;
    created_at: string;
}

interface Props {
    samples: PaginatedData<Sample>;
    filters: { lab_id?: number; priority?: string };
}

export default function Index({ samples, filters }: Props) {
    return (
        <AuthenticatedLayout>
            <Head title="Samples" />
            {/* content */}
        </AuthenticatedLayout>
    );
}
```

### 7.2 Shared Components
```
Components/UI/       → Komponen UI generik (Button, Badge, Modal, Card)
Components/Form/     → Form elements (TextInput, SelectInput, DatePicker)
Components/Table/    → Table components (DataTable, Pagination, SortHeader)
```

### 7.3 Naming Convention Frontend

| Tipe | Format | Contoh |
|---|---|---|
| Page | `PascalCase.tsx` | `Index.tsx`, `Create.tsx` |
| Component | `PascalCase.tsx` | `DataTable.tsx`, `StatusBadge.tsx` |
| Hook | `camelCase.ts` | `useDebounce.ts`, `useFilter.ts` |
| Utility | `camelCase.ts` | `formatDate.ts`, `formatCurrency.ts` |

### 7.4 UI Style Rules (Compact + Enterprise)

Aturan ini **wajib** dipakai untuk semua halaman dashboard/module agar style konsisten:

1. **Visual direction**
- Gunakan gaya **enterprise, clean, compact**.
- Hindari layout “marketing”, ornament berlebihan, dan komponen terlalu besar.

2. **Spacing & density**
- Prioritaskan kepadatan data: gunakan padding/spacing yang efisien.
- Target tinggi komponen:
  - tombol/filter default: `h-8` atau `h-9`
  - tabel compact: cell `py-1.5` sampai `py-2`
- Jarak antar card/filter jangan terlalu longgar.

3. **Typography**
- Heading page: konsisten `text-2xl font-bold tracking-tight`.
- Teks pendukung: `text-sm text-muted-foreground`.
- Data tabel: `text-xs`/`text-sm` sesuai kebutuhan baca.

4. **Color & theming**
- Semua warna wajib berbasis token (`foreground`, `muted-foreground`, `primary`, `border`, dst), **jangan hardcode hitam/putih**.
- Wajib aman di dark mode: icon, teks, badge, pagination harus tetap terbaca tanpa hover/focus.

5. **Table behavior**
- Tabel adalah area utama informasi: tampilkan versi compact secara default.
- Untuk teks panjang gunakan mekanisme expand/collapse (`Lihat selengkapnya` / `Sembunyikan`), bukan truncate permanen.
- Pagination wajib konsisten warna token dan mudah dibaca di light/dark mode.

6. **Header & layout**
- Header/navbar bersifat statis di layout, page hanya render konten `Main`.
- Header tetap sticky saat scroll untuk menjaga konteks user.

7. **Reusable-first**
- Gunakan komponen shared dari `packages/ui-shadcn` terlebih dahulu.
- Jika ada penyesuaian style lintas halaman, update komponen shared, bukan override per-page berulang.

---

## 8. Naming Convention

### 8.1 Backend Files

| Tipe | Format | Contoh |
|---|---|---|
| **Model** | `{Noun}.php` | `Sample.php`, `TestResult.php` |
| **Enum** | `{Model}{Field}.php` | `SampleStatus.php`, `UserRole.php` |
| **Event** | `{Model}{PastVerb}.php` | `SampleApproved.php`, `OrderShipped.php` |
| **Exception** | `{Deskripsi}Exception.php` | `SampleExpiredException.php` |
| **Domain Service** | `{Deskripsi}Service.php` | `PricingService.php`, `SampleAcceptanceCriteria.php` |
| **Value Object** | `{Noun}.php` | `Money.php`, `Email.php`, `ConcentrationUnit.php` |
| **Contract** | `{Model}RepositoryInterface.php` | `SampleRepositoryInterface.php` |
| **Action** | `{Verb}{Model}Action.php` | `CreateSampleAction.php`, `ApproveSampleAction.php` |
| **Query** | `Get{What}{Filter}Query.php` | `GetPendingSamplesQuery.php` |
| **DTO** | `{Verb}{Model}DTO.php` | `CreateSampleDTO.php` |
| **Listener** | `{Deskripsi}Listener.php` | `SendApprovalNotification.php` |
| **Job** | `{Verb}{Model}Job.php` | `GenerateReportJob.php` |
| **Controller** | `{Model}Controller.php` | `SampleController.php` |
| **Request** | `{Verb}{Model}Request.php` | `CreateSampleRequest.php` |
| **Repository** | `Eloquent{Model}Repository.php` | `EloquentSampleRepository.php` |
| **Policy** | `{Model}Policy.php` | `SamplePolicy.php` |

### 8.2 Method Naming

| Method | Kapan Dipakai |
|---|---|
| `execute()` | Action class (satu-satunya public method) |
| `get()` | Query class (satu-satunya public method) |
| `handle()` | Listener dan Job |
| `is{Adjective}()` | Boolean check: `isExpired()`, `isAdmin()` |
| `can{Verb}()` | Permission check: `canBeEdited()`, `canApprove()` |
| `ensure{Condition}()` | Guard/assertion + throw: `ensureNotExpired()` |
| `findBy{Field}()` | Repository lookup: `findByEmail()` |

---

## 9. Alur Kerja: Membuat Fitur Baru

### Step 1: Identifikasi Module
> "Fitur ini milik module apa?" → Kalau belum ada, buat baru: `php artisan make:module {Name}`

### Step 2: Buat Domain Layer dulu
1. Buat/update **Model** + migration
2. Buat **Enum** kalau ada status/tipe
3. Buat **Event** kalau ada yang perlu di-announce
4. Buat **Domain Service** kalau ada aturan bisnis kompleks
5. Buat **Exception** untuk error domain

### Step 3: Buat Application Layer
1. Buat **DTO** untuk input data
2. Buat **Action** untuk write operations
3. Buat **Query** untuk read operations yang kompleks
4. Buat **Listener** kalau ada event yang perlu di-handle

### Step 4: Buat Infrastructure Layer
1. Buat **Request** untuk validasi form
2. Buat **Controller** (tipis! cuma panggil Action/Query)
3. Buat **Route** di `Routes/web.php`
4. Buat **Policy** untuk authorization
5. Update **ServiceProvider** kalau ada binding baru

### Step 5: Buat Frontend
1. Buat **Page** components di `resources/js/Pages/{Module}/`
2. Reuse shared components dari `Components/`

### Step 6: Test
1. **Unit test** untuk Domain Service dan DTO
2. **Feature test** untuk Controller/Route

---

## 10. Komunikasi Antar Module

### ✅ Boleh: Lewat Events
```php
// Module Sample fire event
event(new SampleApproved($sampleId));

// Module Notification dengarkan event ini
class SendSampleApprovalNotification
{
    public function handle(SampleApproved $event): void { ... }
}
```

### ✅ Boleh: Lewat Shared Contracts
```php
// Shared/Application/Contracts/InventoryServiceInterface.php
interface InventoryServiceInterface
{
    public function checkStock(int $productId): int;
    public function reserveStock(int $productId, int $qty): bool;
}

// Module Sample tinggal inject interface ini
class CreateSampleAction
{
    public function __construct(
        private InventoryServiceInterface $inventory,
    ) {}
}
```

### ❌ DILARANG: Direct Import Model dari Module Lain
```php
// ❌ SALAH
use App\Modules\Inventory\Domain\Models\Product;
Product::find($id); // Module Sample TIDAK boleh langsung akses model Inventory

// ✅ BENAR
$this->inventoryService->checkStock($productId); // Lewat contract
```

---

## 11. Testing Strategy

### 11.1 Lokasi Tests
```
app/Modules/{Module}/Tests/
├── Unit/                    ← Test Domain & Application (tanpa database)
│   ├── DTOs/
│   ├── Services/
│   └── Actions/
└── Feature/                 ← Test Infrastructure (dengan database)
    └── Controllers/
```

### 11.2 Apa Yang Di-Test

| Layer | Test Type | Apa yang dicek |
|---|---|---|
| Domain Service | Unit | Aturan bisnis return value yang benar |
| DTO | Unit | `fromArray()` dan `toArray()` benar |
| Action | Unit/Feature | Business logic berjalan + event fired |
| Query | Feature | Query return data yang benar |
| Controller | Feature | HTTP response, redirect, validation |

---

## 12. Panduan Refactoring

### 12.1 Langkah Refactor dari MVC ke Modular DDD

```
1. Identifikasi bounded contexts → list module yang akan dibuat
2. Buat module satu per satu, mulai dari yang PALING independen
3. Untuk setiap module:
   a. Pindahkan Model ke Domain/Models/ (update namespace)
   b. Ekstrak logic dari Controller ke Actions/
   c. Ekstrak query kompleks ke Queries/
   d. Ekstrak validation ke Requests/
   e. Pindahkan routes ke module Routes/
   f. Buat ServiceProvider untuk register routes & bindings
4. Update auth config kalau User model pindah
5. Test setiap module setelah pindah
6. Hapus file lama setelah yakin module baru bekerja
```

### 12.2 Prioritas Refactor untuk LIMS

```
Phase 1: Foundation
├── Shared module (contracts, traits, helpers)
├── Auth / User module
└── Setup permission system (spatie/laravel-permission)

Phase 2: Core LIMS
├── Sample module (registration, tracking)
├── Testing module (test methods, results)
└── Instrument module (equipment, calibration)

Phase 3: Quality & Reporting
├── QualityControl module (specifications, CoA)
├── Report module (lab reports, certificates)
└── Document module (file management)

Phase 4: Supporting
├── Notification module
├── Dashboard module (analytics)
└── AuditLog module (activity tracking)
```

---

## 13. Anti-Pattern (Yang Harus Dihindari)

| ❌ Anti-Pattern | ✅ Solusi |
|---|---|
| Business logic di Controller | Pindahkan ke **Action** atau **Domain Service** |
| Query SQL panjang di Controller | Ekstrak ke **Query** class |
| Model import dari module lain langsung | Gunakan **Events** atau **Shared Contracts** |
| God Model (model 1000+ line) | Pecah logic ke **Domain Service** |
| DTO dengan logic bisnis | DTO hanya data, logic ke **Action/Service** |
| Validation di Action | Validasi input di **FormRequest**, validasi bisnis di **Domain Service** |
| Action memanggil Action lain | Gunakan **Event + Listener** untuk chaining |
| Hardcoded string status | Gunakan **Enum** |

---

## 14. Checklist Review Code

Gunakan checklist ini sebelum merge code:

### Domain Layer
- [ ] Model hanya berisi property, relationship, dan domain method
- [ ] Enum digunakan untuk semua status/tipe (bukan string)
- [ ] Domain tidak import dari Application/Infrastructure
- [ ] Exception memiliki pesan yang jelas

### Application Layer
- [ ] Satu Action = satu use case
- [ ] Action method bernama `execute()`
- [ ] Query method bernama `get()`
- [ ] DTO property semua `readonly`
- [ ] Event di-fire di akhir Action (bukan di tengah)

### Infrastructure Layer
- [ ] Controller tipis (max 10-15 line per method)
- [ ] Read operation pakai Query, write operation pakai Action
- [ ] FormRequest handle validasi input (bukan di Controller)
- [ ] Policy handle authorization
- [ ] Route didefinisikan di module, bukan di `routes/web.php` global

### Umum
- [ ] Tidak ada direct import model dari module lain
- [ ] Naming convention diikuti
- [ ] Test ada untuk setiap Action dan Domain Service

---

## Quick Reference: File Baru Taruh Dimana?

| Saya mau bikin... | Taruh di... |
|---|---|
| Tabel database baru | `Domain/Models/` |
| Status / tipe / kategori | `Domain/Enums/` |
| "Sesuatu terjadi!" | `Domain/Events/` |
| Aturan bisnis kompleks | `Domain/Services/` |
| Error khusus domain | `Domain/Exceptions/` |
| Objek tanpa ID (uang, satuan) | `Domain/ValueObjects/` |
| Interface / kontrak | `Domain/Contracts/` |
| Fitur baru (create/update/delete) | `Application/Actions/` |
| Baca data kompleks | `Application/Queries/` |
| Data input | `Application/DTOs/` |
| Reaksi terhadap event | `Application/Listeners/` |
| Background task | `Application/Jobs/` |
| Endpoint HTTP | `Infrastructure/Controllers/` |
| Validasi form | `Infrastructure/Requests/` |
| Implementasi repository | `Infrastructure/Repositories/` |
| Siapa boleh apa | `Infrastructure/Policies/` |
| Daftar URL | `Infrastructure/Routes/` |
| Registrasi module | `Infrastructure/Providers/` |
| Export data | `Infrastructure/Exports/` |
| Halaman web | `resources/js/Pages/{Module}/` |

---

## 15. Audit Trail

> **Wajib untuk industri regulated** (ISO 17025, GMP, FDA 21 CFR Part 11).
> Setiap create, update, dan delete harus tercatat: **siapa**, **kapan**, **apa yang berubah**.

### 15.1 Package
```bash
composer require spatie/laravel-activitylog
```

### 15.2 Setup di Model
```php
// Domain/Models/Sample.php
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;

class Sample extends Model
{
    use LogsActivity;

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logAll()
            ->logOnlyDirty()
            ->setDescriptionForEvent(fn(string $eventName) =>
                "Sample has been {$eventName}"
            );
    }
}
```

### 15.3 Manual Log di Action
```php
class ApproveSampleAction extends BaseAction
{
    public function execute(Sample $sample, User $approver): Sample
    {
        activity()
            ->performedOn($sample)
            ->causedBy($approver)
            ->withProperties([
                'old_status' => $sample->status->value,
                'new_status' => SampleStatus::Approved->value,
            ])
            ->log('Sample approved');

        $sample->update(['status' => SampleStatus::Approved]);

        return $sample;
    }
}
```

### 15.4 Aturan Audit Trail

| Aturan | Detail |
|---|---|
| Semua model utama WAJIB `use LogsActivity` | Sample, TestResult, Instrument, User |
| Log harus mencatat `causer` (siapa) | Selalu pass `$user` ke Action |
| Jangan delete activity log | Audit trail harus immutable |
| Retention minimal 5 tahun | Sesuaikan dengan regulasi |

---

## 16. Permission & Authorization

> Untuk ERP/LIMS, **role-based saja tidak cukup**. Butuh **permission-based** yang granular.

### 16.1 Package
```bash
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
```

### 16.2 Format Permission

Format: `{module}.{action}`

```
sample.view, sample.create, sample.edit, sample.delete,
sample.submit, sample.approve, sample.reject, sample.release

instrument.view, instrument.calibrate, instrument.approve_calibration

report.view, report.generate, report.sign_off, report.export
```

### 16.3 Gunakan di Policy
```php
class SamplePolicy
{
    public function approve(User $user, Sample $sample): bool
    {
        return $user->hasPermissionTo('sample.approve')
            && $sample->status === SampleStatus::Reviewed;
    }
}
```

### 16.4 Pass Permission ke Frontend
```php
// Di Controller:
return Inertia::render('Sample/Index', [
    'samples' => $samples,
    'can' => [
        'create' => $request->user()->can('create', Sample::class),
        'approve' => $request->user()->hasPermissionTo('sample.approve'),
    ],
]);
```
```tsx
// Di React:
{can.create && <Button>Create Sample</Button>}
```

### 16.5 Contoh Role LIMS

| Role | Permissions |
|---|---|
| **Lab Director** | Semua permission |
| **Quality Manager** | `*.approve`, `*.release`, `report.sign_off` |
| **Analyst** | `sample.view/create/edit/submit` |
| **Lab Technician** | `sample.view`, `instrument.calibrate` |
| **Auditor** | Semua `*.view`, `report.export` |

---

## 17. State Machine & Workflow

> Status transition harus **strict** - tidak boleh loncat dari Draft langsung ke Approved.

### 17.1 Definisikan Transisi di Enum

```php
enum SampleStatus: string
{
    case Draft = 'draft';
    case Submitted = 'submitted';
    case InTesting = 'in_testing';
    case Reviewed = 'reviewed';
    case Approved = 'approved';
    case Rejected = 'rejected';
    case Released = 'released';

    public function allowedTransitions(): array
    {
        return match ($this) {
            self::Draft      => [self::Submitted],
            self::Submitted  => [self::InTesting, self::Rejected],
            self::InTesting  => [self::Reviewed],
            self::Reviewed   => [self::Approved, self::Rejected],
            self::Approved   => [self::Released],
            self::Rejected   => [self::Draft],
            self::Released   => [],
        };
    }

    public function canTransitionTo(self $newStatus): bool
    {
        return in_array($newStatus, $this->allowedTransitions());
    }
}
```

### 17.2 Guard di Domain Service

```php
class SampleWorkflowService
{
    public function transition(Sample $sample, SampleStatus $newStatus, User $actor): void
    {
        if (!$sample->status->canTransitionTo($newStatus)) {
            throw new InvalidStatusTransitionException(
                $sample->status->value, $newStatus->value
            );
        }

        match ($newStatus) {
            SampleStatus::Approved => $this->guardApprove($sample, $actor),
            SampleStatus::Released => $this->guardRelease($sample),
            default => null,
        };

        $sample->update(['status' => $newStatus]);
    }

    private function guardApprove(Sample $sample, User $actor): void
    {
        if ($sample->submitted_by === $actor->id) {
            throw new SamePersonApprovalException(
                'Submitter and approver cannot be the same person.'
            );
        }
    }

    private function guardRelease(Sample $sample): void
    {
        if ($sample->testResults()->whereNull('value')->exists()) {
            throw new IncompleteTestResultsException();
        }
    }
}
```

### 17.3 Workflow Diagram

```
Draft --> Submitted --> In Testing --> Reviewed --> Approved --> Released
                |                          |
             Rejected                   Rejected
                |                          |
              Draft                      Draft
```

**Aturan**: Jangan ubah status langsung via `update()`. Selalu lewat `WorkflowService::transition()`.

---

## 18. Error Handling Strategy

### 18.1 Tipe Exception per Layer

| Layer | Base Class | Contoh |
|---|---|---|
| **Domain** | `\DomainException` | `SampleExpiredException`, `InvalidStatusTransitionException` |
| **Application** | `\RuntimeException` | `InsufficientStockException`, `ExportFailedException` |
| **Infrastructure** | Laravel built-in | `ValidationException`, `AuthorizationException` |

### 18.2 Pattern

```php
class InvalidStatusTransitionException extends \DomainException
{
    public function __construct(
        public readonly string $from,
        public readonly string $to,
    ) {
        parent::__construct("Cannot transition from '{$from}' to '{$to}'.");
    }
}
```

### 18.3 Global Handler

```php
// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->renderable(function (\DomainException $e, Request $request) {
        if ($request->wantsJson()) {
            return response()->json(['message' => $e->getMessage()], 422);
        }
        return back()->with('error', $e->getMessage());
    });
})
```

**Aturan**: Domain exception = user melanggar aturan bisnis (bukan bug). Tampilkan pesan user-friendly.

---

## 19. Database Conventions

> **Konvensi ini berlaku cross-platform**: Laravel (web), Desktop, Android.
> Semua platform menggunakan nama tabel yang **sama persis** di database.

### 19.1 Format Nama Tabel: `{app}_{type}_{nama}`

| Bagian | Aturan | Contoh |
|---|---|---|
| `{app}` | Singkatan aplikasi, 3-5 huruf kecil | `lims`, `erp`, `hris` |
| `{type}` | Tipe tabel (lihat 19.2) | `m_`, `t_`, `r_`, `h_`, `p_` |
| `{nama}` | **WAJIB plural**, snake_case | `samples`, `test_results`, `users` |

> 🔴 **Aturan: Nama tabel HARUS plural (jamak). Tidak ada pengecualian.**

```
-- ❌ SALAH (singular)
lims_m_sample
lims_t_test_result
lims_m_user
lims_r_unit

-- ✅ BENAR (plural)
lims_m_samples
lims_t_test_results
lims_m_users
lims_r_units
```

> **Catatan pivot table** (`p_`): Gunakan **dua kata singular** diurutkan **alphabetical** + plural suffix opsional.
> Contoh: `lims_p_instrument_samples` (instrument + samples, `i` sebelum `s`).

### 19.2 Tipe Tabel (Prefix)

| Prefix | Tipe | Deskripsi | Contoh |
|---|---|---|---|
| `m_` | **Master** | Data utama / entitas bisnis | `lims_m_samples`, `lims_m_instruments`, `lims_m_users` |
| `t_` | **Transaction** | Data kejadian / aktivitas | `lims_t_test_results`, `lims_t_calibrations` |
| `r_` | **Reference** | Lookup / dropdown (jarang berubah) | `lims_r_units`, `lims_r_test_methods`, `lims_r_departments` |
| `h_` | **History** | Log, audit trail, archive | `lims_h_activity_logs`, `lims_h_status_changes` |
| `p_` | **Pivot** | Tabel relasi many-to-many | `lims_p_instrument_samples`, `lims_p_role_permissions` |

### 19.3 Contoh Lengkap (LIMS)

```
-- Master
lims_m_users
lims_m_samples
lims_m_instruments
lims_m_customers

-- Reference / Lookup
lims_r_units                    (ppm, mg/L, %)
lims_r_test_methods             (metode uji)
lims_r_sample_types             (jenis sample)
lims_r_departments

-- Transaction
lims_t_test_results
lims_t_calibrations
lims_t_sample_submissions
lims_t_approvals

-- History / Audit
lims_h_activity_logs
lims_h_status_changes

-- Pivot
lims_p_sample_instrument
lims_p_role_permission
lims_p_user_department
```

### 19.4 Setup di Laravel

**JANGAN** pakai config prefix. Tulis nama tabel lengkap di setiap Model:

```php
// config/database.php
'mysql' => [
    'prefix' => '',   // Kosong! Tidak pakai auto-prefix
],
```

```php
// Setiap model WAJIB set $table secara eksplisit
class Sample extends Model
{
    protected $table = 'lims_m_samples';
}

class TestResult extends Model
{
    protected $table = 'lims_t_test_results';
}

class Unit extends Model
{
    protected $table = 'lims_r_units';
}
```

> **Kenapa tidak pakai config prefix?**
> Config prefix adalah fitur Laravel-only. Desktop app dan Android melihat nama tabel asli
> di database. Dengan menulis nama lengkap di Model, semua platform konsisten.

### 19.5 Kolom Standard

> Setiap tabel WAJIB memiliki kolom-kolom standard berikut sesuai tipe tabelnya.

#### Kolom Wajib (Semua Tabel)

| Kolom | Tipe | Keterangan |
|---|---|---|
| `id` | `bigIncrements` | Primary key, auto increment |
| `uuid` | `uuid`, unique | Identifier untuk API & cross-platform (desktop/android) |
| `created_by` | `unsignedBigInteger`, nullable | ID user yang membuat record |
| `updated_by` | `unsignedBigInteger`, nullable | ID user yang terakhir mengubah |
| `created_at` | `timestamp` | Otomatis oleh Laravel |
| `updated_at` | `timestamp` | Otomatis oleh Laravel |

> **Kenapa `uuid`?** `id` auto increment tidak aman untuk di-expose di URL/API
> (bisa ditebak: 1, 2, 3...). `uuid` aman untuk frontend, mobile, dan API partner.

#### Kolom Standard: Master Tables (`m_`)

| Kolom | Tipe | Keterangan |
|---|---|---|
| `code` | `varchar(50)`, unique | Kode referensi (SMP-001, INS-042) |
| `name` | `varchar(255)` | Nama item |
| `description` | `text`, nullable | Deskripsi opsional |
| `is_active` | `boolean`, default `true` | Status aktif/nonaktif |
| `deactivated_at` | `timestamp`, nullable | Tanggal dinonaktifkan |
| `deactivated_by` | `unsignedBigInteger`, nullable | ID user yang menonaktifkan |
| `deleted_at` | `timestamp`, nullable | Soft delete (SoftDeletes trait) |
| `deleted_by` | `unsignedBigInteger`, nullable | ID user yang menghapus |

```php
// Contoh migration: lims_m_samples
Schema::create('lims_m_samples', function (Blueprint $table) {
    $table->id();
    $table->uuid('uuid')->unique();
    $table->string('code', 50)->unique();          // SMP-2024-001
    $table->string('name');
    $table->text('description')->nullable();
    $table->string('status')->default('draft');     // Enum cast
    $table->boolean('is_active')->default(true);
    $table->timestamp('deactivated_at')->nullable();
    $table->unsignedBigInteger('deactivated_by')->nullable();

    // ... kolom spesifik module ...

    $table->unsignedBigInteger('created_by')->nullable();
    $table->unsignedBigInteger('updated_by')->nullable();
    $table->unsignedBigInteger('deleted_by')->nullable();
    $table->timestamps();
    $table->softDeletes();

    $table->index('status');
    $table->index('is_active');
});
```

#### Kolom Standard: Transaction Tables (`t_`)

| Kolom | Tipe | Keterangan |
|---|---|---|
| `transaction_date` | `date` / `datetime` | Tanggal transaksi terjadi |
| `status` | `varchar` | Status workflow (Enum cast) |
| `notes` | `text`, nullable | Catatan operator/analyst |
| `deleted_at` | `timestamp`, nullable | Soft delete |
| `deleted_by` | `unsignedBigInteger`, nullable | ID user yang menghapus |

```php
// Contoh migration: lims_t_test_results
Schema::create('lims_t_test_results', function (Blueprint $table) {
    $table->id();
    $table->uuid('uuid')->unique();
    $table->foreignId('sample_id')->constrained('lims_m_samples');
    $table->string('status')->default('pending');
    $table->date('transaction_date');
    $table->text('notes')->nullable();

    // ... kolom spesifik module ...

    $table->unsignedBigInteger('created_by')->nullable();
    $table->unsignedBigInteger('updated_by')->nullable();
    $table->unsignedBigInteger('deleted_by')->nullable();
    $table->timestamps();
    $table->softDeletes();

    $table->index('status');
    $table->index('transaction_date');
});
```

#### Kolom Standard: Reference Tables (`r_`)

| Kolom | Tipe | Keterangan |
|---|---|---|
| `code` | `varchar(50)`, unique | Kode singkat (ppm, mg/L) |
| `name` | `varchar(255)` | Nama lengkap |
| `is_active` | `boolean`, default `true` | Untuk hide/show di dropdown |
| `deactivated_at` | `timestamp`, nullable | Tanggal dinonaktifkan |
| `deactivated_by` | `unsignedBigInteger`, nullable | ID user yang menonaktifkan |

```php
// Contoh migration: lims_r_units
Schema::create('lims_r_units', function (Blueprint $table) {
    $table->id();
    $table->uuid('uuid')->unique();
    $table->string('code', 50)->unique();   // ppm, mg/L, %
    $table->string('name');                 // Parts per million
    $table->boolean('is_active')->default(true);
    $table->timestamp('deactivated_at')->nullable();
    $table->unsignedBigInteger('deactivated_by')->nullable();

    $table->unsignedBigInteger('created_by')->nullable();
    $table->unsignedBigInteger('updated_by')->nullable();
    $table->timestamps();
    // Reference biasanya TIDAK pakai soft delete
});
```

#### Kolom Standard: History Tables (`h_`)

History bersifat **append-only** (hanya insert, TIDAK pernah update/delete):

```
-- Biasanya otomatis dari spatie/laravel-activitylog
-- Tidak perlu buat migration manual
```

#### Ringkasan Kolom Per Tipe Tabel

| Kolom | Master (`m_`) | Transaction (`t_`) | Reference (`r_`) | History (`h_`) |
|---|---|---|---|---|
| `id` | ✅ | ✅ | ✅ | ✅ |
| `uuid` | ✅ | ✅ | ✅ | ❌ |
| `code` | ✅ | ❌ | ✅ | ❌ |
| `name` | ✅ | ❌ | ✅ | ❌ |
| `status` | ✅ | ✅ | ❌ | ❌ |
| `is_active` | ✅ | ❌ | ✅ | ❌ |
| `deactivated_at` | ✅ | ❌ | ✅ | ❌ |
| `deactivated_by` | ✅ | ❌ | ✅ | ❌ |
| `notes` | opsional | ✅ | ❌ | ❌ |
| `transaction_date` | ❌ | ✅ | ❌ | ❌ |
| `created_by` | ✅ | ✅ | ✅ | ✅ |
| `updated_by` | ✅ | ✅ | ✅ | ❌ |
| `deleted_by` | ✅ | ✅ | ❌ | ❌ |
| `created_at` | ✅ | ✅ | ✅ | ✅ |
| `updated_at` | ✅ | ✅ | ✅ | ❌ |
| `deleted_at` | ✅ | ✅ | ❌ | ❌ |

#### Konvensi Penamaan Kolom

| Convention | Format | Contoh |
|---|---|---|
| Primary key | `id` | `id` |
| Foreign key | `{singular_model}_id` | `sample_id`, `user_id` |
| Status | VARCHAR (untuk Enum cast) | `status`, bukan integer |
| Boolean | `is_{adjective}` | `is_active`, `is_verified` |
| Tanggal kejadian | `{action}_at` | `approved_at`, `deactivated_at` |
| Pelaku kejadian | `{action}_by` | `created_by`, `deactivated_by` |

### 19.6 Migration

```
Lokasi: database/migrations/ (tetap global, BUKAN di dalam module)
Alasan: migration harus urut dan bisa dijalankan secara global

Format nama: {timestamp}_create_{full_table_name}_table.php
Contoh:      2024_01_15_create_lims_m_samples_table.php
```

### 19.7 Index Strategy

```php
// Index untuk kolom yang sering di-WHERE, ORDER BY, dan filter:
$table->index('status');
$table->index('batch_number');
$table->index(['laboratory_id', 'status']);  // Composite index
```

### 19.8 Seeder per Module

```
database/seeders/
+-- DatabaseSeeder.php           (panggil semua module seeder)
+-- UserModuleSeeder.php
+-- SampleModuleSeeder.php
+-- ReferenceDataSeeder.php      (seed semua tabel r_: units, methods, dll)
+-- PermissionSeeder.php         (seed permission dan role)
```

---

## 20. Recommended Packages

---

### 🐘 PHP (Composer)

#### Wajib

| Package | Fungsi | Install |
|---|---|---|
| `spatie/laravel-permission` | Role & Permission granular (RBAC) | `composer require spatie/laravel-permission` |
| `maatwebsite/excel` | Export & import Excel (laporan, data massal) | `composer require maatwebsite/excel` |
| `barryvdh/laravel-dompdf` | Generate PDF (CoA, laporan, invoice) | `composer require barryvdh/laravel-dompdf` |
| `laravel/tinker` | REPL untuk debugging di development | `composer require laravel/tinker` |

#### Opsional (sesuai kebutuhan)

| Package | Fungsi | Install |
|---|---|---|
| `spatie/laravel-activitylog` | Audit trail otomatis (log setiap create/update/delete) | `composer require spatie/laravel-activitylog` |
| `spatie/laravel-medialibrary` | Upload & manage file/attachment | `composer require spatie/laravel-medialibrary` |
| `simplesoftwareio/simple-qrcode` | Generate QR code (label sample, barcode) | `composer require simplesoftwareio/simple-qrcode` |
| `spatie/laravel-backup` | Backup database & storage | `composer require spatie/laravel-backup` |
| `sentry/sentry-laravel` | Error monitoring di production | `composer require sentry/sentry-laravel` |
| `stancl/tenancy` | Multi-company / multi-lab (multi-tenant) | `composer require stancl/tenancy` |

---

### 🟨 JavaScript (npm)

#### Wajib

| Package | Fungsi | Install |
|---|---|---|
| `shadcn/ui` | Komponen UI siap pakai (berbasis Radix UI) | `npx shadcn@latest init` |
| `@radix-ui/react-icons` | Icon set resmi Radix UI | `npm install @radix-ui/react-icons` |
| `lucide-react` | Icon set modern untuk React | `npm install lucide-react` |
| `@tanstack/react-table` | Headless table: sorting, filter, pagination | `npm install @tanstack/react-table` |
| `axios` | HTTP client untuk API request | `npm install axios` |
| `date-fns` | Utility manipulasi & format tanggal | `npm install date-fns` |
| `recharts` | Chart & grafik untuk dashboard | `npm install recharts` |
| `sonner` | Toast notification yang modern | `npm install sonner` |
| `zod` | Schema validation untuk form & API | `npm install zod` |
| `react-hook-form` | Form state management yang performant | `npm install react-hook-form` |
| `zustand` | State management global yang ringan | `npm install zustand` |

#### Install sekaligus

```bash
npm install @radix-ui/react-icons lucide-react @tanstack/react-table axios date-fns recharts sonner zod react-hook-form zustand
npx shadcn@latest init
```

---

### Aturan Umum Pakai Package

| Aturan | Detail |
|---|---|
| Trait di **Domain/Models** | `LogsActivity`, `HasRoles`, `HasMedia` boleh ditaruh di Model |
| Facade di **Infrastructure** | `Excel::download()`, `PDF::loadView()` hanya di Controller/Export |
| Jangan pakai package di **Action** | Action tetap bersih dari framework-specific code |
| `zod` + `react-hook-form` SELALU berpasangan | Validasi schema frontend wajib pakai keduanya |
| `zustand` hanya untuk state lintas komponen | State lokal tetap pakai `useState` |


---

## 21. Git & Version Control

### 21.1 Branching Model

Gunakan **simplified GitFlow** yang cocok untuk tim kecil (3 orang):

```
main              ← Production (hanya dari merge develop/hotfix)
  │
develop           ← Staging / integrasi (default branch untuk develop)
  │
  ├── feature/*   ← Fitur baru
  ├── fix/*        ← Bug fix
  ├── refactor/*   ← Refactoring tanpa fitur baru
  └── hotfix/*     ← Fix urgent di production (merge ke main + develop)
```

**Aturan:**

| Aturan | Detail |
|---|---|
| Branch dari `develop` | Semua feature/fix branch dibuat dari `develop` |
| Merge ke `develop` via Pull Request | Tidak boleh push langsung ke `develop` |
| `main` hanya dari `develop` atau `hotfix` | Tidak boleh push langsung ke `main` |
| Hapus branch setelah merge | Jaga repository tetap bersih |

### 21.2 Branch Naming

```
Format: {type}/{module}-{deskripsi-singkat}

Contoh:
feature/sample-crud-management
feature/instrument-calibration-workflow
fix/user-login-redirect-error
refactor/sample-extract-to-query
hotfix/report-pdf-blank-page
```

### 21.3 Commit Message Format

Gunakan **Conventional Commits**:

```
{type}({module}): {deskripsi singkat}

Contoh:
feat(sample): add approval workflow
fix(user): fix login redirect after session timeout
refactor(instrument): extract calibration logic to domain service
docs(shared): update architecture guide
test(sample): add unit test for CreateSampleDTO
chore(deps): update laravel to 12.1
style(sample): fix code formatting
```

**Type yang diperbolehkan:**

| Type | Kapan Dipakai |
|---|---|
| `feat` | Fitur baru |
| `fix` | Bug fix |
| `refactor` | Reorganisasi kode tanpa ubah behavior |
| `docs` | Dokumentasi |
| `test` | Tambah/update test |
| `chore` | Maintenance (update deps, config) |
| `style` | Formatting, missing semicolons (bukan CSS) |
| `perf` | Peningkatan performa |

### 21.4 Pull Request (PR) Rules

| Aturan | Detail |
|---|---|
| Minimal **1 reviewer** approve | Sebelum merge ke `develop` |
| PR harus lulus CI (test + lint) | Otomatis dari GitHub Actions |
| Judul PR ikuti format commit | `feat(sample): add approval workflow` |
| Deskripsi PR wajib ada | Jelaskan: apa yang berubah, kenapa, cara test |
| Satu PR = satu fitur/fix | Jangan campur banyak fitur dalam 1 PR |

### 21.5 Gitignore

File/folder yang **WAJIB** di-ignore:

```gitignore
# Environment
.env
.env.*.local

# Dependencies
/vendor/
/node_modules/

# Build
/public/build/
/public/hot

# IDE
.idea/
.vscode/
*.swp

# OS
.DS_Store
Thumbs.db

# Laravel
/storage/*.key
/storage/logs/*
/bootstrap/cache/*

# Testing
.phpunit.result.cache
coverage/
```

---

## 22. Environment & Configuration

### 22.1 File Environment

```
.env                    ← Local development (JANGAN commit!)
.env.example            ← Template (WAJIB commit, tanpa value sensitif)
.env.testing            ← Untuk PHPUnit (opsional)
```

### 22.2 Variabel Wajib di `.env.example`

```env
# === Application ===
APP_NAME="LIMS"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8000

# === Database ===
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=lims_db
DB_USERNAME=root
DB_PASSWORD=

# === Cache & Session ===
CACHE_STORE=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync

# === Mail ===
MAIL_MAILER=log
MAIL_FROM_ADDRESS="noreply@lims.local"

# === Filesystem ===
FILESYSTEM_DISK=local
```

### 22.3 Aturan Environment

| Aturan | Detail |
|---|---|
| 🔴 JANGAN commit `.env` ke git | Berisi password, API key, secret |
| ✅ SELALU commit `.env.example` | Template tanpa value sensitif |
| Password/secret di production | Gunakan vault atau environment variables server |
| APP_DEBUG=false di production | Wajib! Jangan pernah `true` di production |
| APP_KEY harus unique per environment | Generate: `php artisan key:generate` |

### 22.4 Onboarding: Setup Project Baru

Buat file `README.md` di root project dengan langkah-langkah ini:

```bash
# 1. Clone repository
git clone <repo-url>
cd <project-name>

# 2. Install PHP dependencies
composer install

# 3. Install JavaScript dependencies
npm install

# 4. Setup environment
cp .env.example .env
php artisan key:generate

# 5. Setup database
# (Edit .env dulu: DB_DATABASE, DB_USERNAME, DB_PASSWORD)
php artisan migrate
php artisan db:seed

# 6. Build frontend
npm run build

# 7. Jalankan development server
composer dev
```

### 22.5 Requirement

Dokumentasikan di `README.md`:

```
## Requirements

- PHP >= 8.2
- Composer >= 2.x
- Node.js >= 20.x
- npm >= 10.x
- MySQL >= 8.0 atau PostgreSQL >= 15
```

---

## 23. Code Style & Formatting

### 23.1 PHP: Laravel Pint

Laravel Pint sudah include di Laravel 12. Konfigurasi di `pint.json`:

```json
{
    "preset": "laravel",
    "rules": {
        "concat_space": {
            "spacing": "one"
        },
        "ordered_imports": {
            "sort_algorithm": "alpha"
        },
        "single_trait_insert_per_statement": true,
        "trailing_comma_in_multiline": {
            "elements": ["arguments", "arrays", "parameters"]
        }
    }
}
```

**Cara pakai:**

```bash
# Check tanpa ubah file
./vendor/bin/pint --test

# Fix otomatis
./vendor/bin/pint
```

### 23.2 JavaScript/TypeScript: ESLint + Prettier

```bash
npm install -D eslint prettier eslint-config-prettier eslint-plugin-react
```

File `.prettierrc`:

```json
{
    "semi": true,
    "singleQuote": true,
    "tabWidth": 4,
    "trailingComma": "all",
    "printWidth": 100,
    "bracketSpacing": true
}
```

### 23.3 EditorConfig

Buat `.editorconfig` di root project agar konsisten di semua editor/IDE:

```ini
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.{yml,yaml}]
indent_size = 2

[*.{js,jsx,ts,tsx,json,css}]
indent_size = 4
```

### 23.4 Kapan Jalankan Formatter

| Kapan | Cara |
|---|---|
| Sebelum commit (manual) | `./vendor/bin/pint` dan `npx prettier --write .` |
| Di CI/CD pipeline | `./vendor/bin/pint --test` dan `npx prettier --check .` |
| Otomatis (opsional) | Setup Git pre-commit hook dengan Husky |

### 23.5 Aturan Tambahan

| Aturan | Detail |
|---|---|
| Import diurutkan alphabetical | Auto oleh Pint dan ESLint |
| Satu class per file | Tidak boleh ada 2 class dalam 1 file |
| Strict types di PHP | Tambahkan `declare(strict_types=1);` di setiap file PHP |
| TypeScript strict mode | `"strict": true` di tsconfig.json |
| Tidak boleh ada `any` di TypeScript | Gunakan tipe yang spesifik |
| Tidak boleh ada `dd()` / `console.log()` di commit | Hanya untuk debug lokal |

---

## 24. Security Checklist

### 24.1 CSRF Protection

```
Laravel sudah otomatis handle CSRF via middleware VerifyCsrfToken.
Inertia.js: CSRF token otomatis included, TIDAK perlu tambah manual.
```

**Aturan**: JANGAN disable CSRF middleware kecuali untuk webhook endpoint yang memang stateless.

### 24.2 SQL Injection

```php
// ✅ BENAR: Gunakan Eloquent atau query builder (auto-escaped)
User::where('email', $request->email)->first();
DB::table('users')->where('email', '=', $email)->get();

// ❌ SALAH: Raw query tanpa binding
DB::select("SELECT * FROM users WHERE email = '$email'");

// ✅ BENAR: Raw query DENGAN binding
DB::select("SELECT * FROM users WHERE email = ?", [$email]);
```

**Aturan**: SELALU gunakan Eloquent/Query Builder. Kalau harus raw query, WAJIB pakai parameter binding.

### 24.3 XSS (Cross-Site Scripting)

```php
// ✅ BENAR: Blade auto-escape
{{ $user->name }}           // Auto-escaped

// ❌ SALAH: Unescaped output
{!! $user->bio !!}          // Hanya kalau YAKIN isinya safe HTML
```

```tsx
// React/Inertia: Otomatis safe karena React escape by default
// ❌ SALAH di React:
<div dangerouslySetInnerHTML={{ __html: userInput }} />  // JANGAN!
```

**Aturan**: Jangan pernah render user input sebagai raw HTML.

### 24.4 Mass Assignment

```php
// ✅ BENAR: Definisikan $fillable di setiap Model
class Sample extends Model
{
    protected $fillable = ['name', 'batch_number', 'status'];
}

// ❌ SALAH: Pakai $guarded = [] (izinkan semua field)
class Sample extends Model
{
    protected $guarded = [];   // JANGAN! Semua field bisa di-mass assign
}
```

**Aturan**: SELALU gunakan `$fillable` (whitelist), JANGAN `$guarded = []`.

### 24.5 File Upload

```php
// 1. Validasi tipe file
'file' => ['required', 'file', 'mimes:pdf,xlsx,docx,jpg,png', 'max:10240']

// 2. Simpan di storage PRIVATE, bukan public
Storage::disk('local')->put('samples/attachments', $file);  // ← private

// 3. Generate nama file random, JANGAN pakai nama asli
$path = $file->store('samples/attachments');  // Laravel auto-generate nama

// 4. Serve file via controller (bukan direct URL)
return Storage::download($path);
```

### 24.6 Authentication & Session

| Aturan | Detail |
|---|---|
| Password hashing | SELALU `bcrypt()` atau `Hash::make()` — JANGAN simpan plaintext |
| Session timeout | Set `SESSION_LIFETIME=120` (menit) di `.env` |
| Remember me | Hati-hati untuk ERP/LIMS yang sensitive |
| Login throttling | Laravel auto-handle via `ThrottleRequests` middleware |
| Logout invalidate session | `$request->session()->invalidate()` |

### 24.7 Rate Limiting

```php
// Untuk endpoint sensitif (login, forgot password):
RateLimiter::for('login', function (Request $request) {
    return Limit::perMinute(5)->by($request->ip());
});

// Di route:
Route::post('/login', ...)->middleware('throttle:login');
```

### 24.8 Data yang TIDAK BOLEH di-log

| JANGAN log ini | Alasan |
|---|---|
| Password (plain atau hash) | Security risk |
| Token / API key | Bisa disalahgunakan |
| Nomor kartu kredit | PCI compliance |
| Data medis pasien (kalau ada) | Privasi |
| Full request body tanpa filter | Bisa berisi password |

```php
// Di Model, exclude field sensitif dari activity log:
public function getActivitylogOptions(): LogOptions
{
    return LogOptions::defaults()
        ->logExcept(['password', 'remember_token', 'api_key']);
}
```

### 24.9 Quick Security Checklist (Sebelum Deploy)

- [ ] `APP_DEBUG=false`
- [ ] `APP_ENV=production`
- [ ] APP_KEY di-generate dan unik
- [ ] CSRF middleware aktif
- [ ] Semua password di-hash (tidak ada plaintext)
- [ ] File upload divalidasi (tipe + ukuran)
- [ ] Tidak ada `dd()`, `dump()`, `console.log()` di kode
- [ ] Tidak ada `$guarded = []` di model
- [ ] Rate limiting aktif di login dan API
- [ ] `.env` tidak ter-commit ke git
- [ ] Database credentials tidak hardcoded
- [ ] HTTPS aktif di production
- [ ] Log tidak berisi data sensitif


---

## 25. Multi-App Platform & SSO

> Arsitektur untuk menjalankan **beberapa aplikasi** (LIMS, ERP, HRIS, dll)
> dengan **satu login terpusat** dan **satu database user**.

### 25.1 Overview Arsitektur

```
                     ┌─────────────────────────┐
                     │   AUTH PLATFORM          │
                     │   (Laravel + React)      │
                     │                          │
                     │   auth.domain.com        │
                     │   Database: auth_db      │
                     │                          │
                     │   • Login / Register     │
                     │   • User management      │
                     │   • Role & Permission     │
                     │   • API Token (Sanctum)   │
                     │   • App management        │
                     └─────────┬───────────────┘
                               │
            ┌──────────────────┼──────────────────┐
            │                  │                   │
            ▼                  ▼                   ▼
     ┌─────────────┐   ┌─────────────┐    ┌─────────────┐
     │ LIMS        │   │ ERP         │    │ HRIS        │
     │ (React)     │   │ (Blade)     │    │ (React)     │
     │             │   │             │    │             │
     │ lims.       │   │ erp.        │    │ hris.       │
     │ domain.com  │   │ domain.com  │    │ domain.com  │
     │ DB: lims_db │   │ DB: erp_db  │    │ DB: hris_db │
     └─────────────┘   └─────────────┘    └─────────────┘

     Future:
     ┌──────────┐  ┌──────────┐  ┌──────────┐
     │ Mobile   │  │ Desktop  │  │ Golang   │
     │ App      │  │ App      │  │ Services │
     │ (Token)  │  │ (Token)  │  │ (Token)  │
     └──────────┘  └──────────┘  └──────────┘
```

### 25.2 Monorepo Folder Structure

```
company-platform/                      <- ROOT MONOREPO (1 git repository)
|
|-- apps/                              <- Semua aplikasi
|   |-- auth/                          <- SSO Auth Service (Laravel + React Inertia)
|   |   +-- (Laravel project, DDD modular monolith)
|   |
|   |-- lims/                          <- LIMS (Laravel + React Inertia)
|   |   +-- (Laravel project, DDD modular monolith)
|   |
|   |-- erp/                           <- ERP (Laravel + Blade)
|   |   +-- (Laravel project)
|   |
|   |-- hris/                          <- HRIS (Laravel + React Inertia)
|   |   +-- (Laravel project)
|   |
|   +-- portal/                        <- Customer/Vendor Portal (opsional)
|       +-- (Vite + React SPA)
|
|-- services/                          <- Golang Microservices (Fase 3+)
|   |-- pdf-generator/                 <- Generate PDF/report berat
|   |-- file-processor/                <- Process Excel import besar
|   +-- notification/                  <- Email, SMS, push notification
|
|-- packages/                          <- Shared Code (dipakai semua apps)
|   |-- ui/                            <- Shared React components (DataTable, dll)
|   |-- types/                         <- Shared TypeScript types/interfaces
|   |-- utils/                         <- Shared JS helpers (formatDate, dll)
|   +-- laravel-core/                  <- Shared Laravel package (traits, base classes)
|
|-- mobile/                            <- Mobile Apps (Fase 4+)
|   |-- android/
|   +-- ios/
|
|-- desktop/                           <- Desktop Apps (Fase 4+)
|   +-- tauri-app/
|
|-- docker/                            <- Docker configs (saat migrasi ke VPS)
|   |-- docker-compose.yml
|   |-- nginx/
|   +-- golang/
|
|-- docs/                              <- Documentation
|   +-- ARCHITECTURE_GUIDE.md          <- File ini
|
|-- turbo.json                         <- Turborepo config
|-- package.json                       <- Root package.json (workspaces)
+-- README.md
```

**Fase build:**

| Fase | Yang Dibuat | Timeline |
|---|---|---|
| **Fase 1** | `apps/auth/` + `packages/laravel-core/` + `docs/` | Minggu 1-2 |
| **Fase 2** | `apps/lims/` + `packages/ui/` | Minggu 3+ |
| **Fase 3** | Hubungkan project lama (ERP, HRIS) | Setelah LIMS stable |
| **Fase 4** | `services/` (Golang) | Kalau Laravel kurang cepat |
| **Fase 5** | `mobile/` + `desktop/` | Setelah API stable |

> **Penting**: Tidak semua folder harus ada dari awal. Buat sesuai fase.
> Folder yang belum dipakai cukup buat `.gitkeep` sebagai placeholder.

### 25.3 Shared Packages

**`packages/ui/`** — Komponen React yang dipakai di semua app:

```
packages/ui/
|-- package.json              <- name: "@company/ui"
|-- src/
|   |-- DataTable/
|   |   +-- DataTable.tsx     <- Dipakai LIMS, HRIS, Portal
|   |-- StatusBadge.tsx
|   |-- Modal.tsx
|   +-- index.ts              <- Export semua komponen
+-- tsconfig.json
```

**`packages/laravel-core/`** — Shared PHP code untuk semua app Laravel:

```
packages/laravel-core/
|-- composer.json             <- name: "company/laravel-core"
|-- src/
|   |-- Traits/
|   |   |-- HasUuidTrait.php
|   |   +-- HasAuditColumns.php   <- Auto-fill created_by, updated_by
|   |-- Actions/BaseAction.php
|   |-- Contracts/QueryInterface.php
|   +-- Middleware/
|       +-- EnsureUserHasAppAccess.php
+-- tests/
```

**Cara import di app:**

```json
// apps/lims/package.json
{
    "dependencies": {
        "@company/ui": "workspace:*"
    }
}
```

```json
// apps/lims/composer.json
{
    "repositories": [{
        "type": "path",
        "url": "../../packages/laravel-core"
    }],
    "require": {
        "company/laravel-core": "*"
    }
}
```

### 25.4 Turborepo Config

```json
// turbo.json (root)
{
    "$schema": "https://turbo.build/schema.json",
    "tasks": {
        "build": {
            "dependsOn": ["^build"],
            "outputs": ["public/build/**", "dist/**"]
        },
        "dev": {
            "cache": false,
            "persistent": true
        },
        "lint": {},
        "test": {}
    }
}
```

```json
// package.json (root)
{
    "name": "company-platform",
    "private": true,
    "workspaces": [
        "apps/*",
        "packages/*"
    ],
    "devDependencies": {
        "turbo": "latest"
    },
    "scripts": {
        "dev:auth": "turbo run dev --filter=auth",
        "dev:lims": "turbo run dev --filter=lims",
        "build": "turbo run build",
        "build:auth": "turbo run build --filter=auth",
        "build:lims": "turbo run build --filter=lims"
    }
}
```

### 25.5 Auth Database Schema (`auth_db`)

**Database `auth_db` berisi HANYA data otentikasi dan otorisasi.**
Semua app terhubung ke database ini untuk login & permission.

```
auth_m_users                  ← Data user terpusat
auth_m_roles                  ← Role global + per-app
auth_m_permissions            ← Permission per app (lims.sample.create)
auth_m_applications           ← Daftar app yang terhubung (LIMS, ERP, HRIS)
auth_p_role_permissions       ← Role X punya permission apa
auth_p_user_roles             ← User X punya role apa
auth_p_user_applications      ← User X punya akses ke app apa
auth_h_sessions               ← Session aktif (shared semua app)
auth_h_login_histories        ← Log login untuk audit/security
auth_h_personal_access_tokens ← API tokens (Sanctum) untuk mobile/desktop
```

**Tabel `auth_m_applications`:**
```sql
CREATE TABLE auth_m_applications (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    uuid CHAR(36) UNIQUE,
    code VARCHAR(50) UNIQUE,         -- 'lims', 'erp', 'hris'
    name VARCHAR(255),               -- 'Laboratory Information Management System'
    url VARCHAR(255),                -- 'https://lims.domain.com'
    description TEXT NULL,
    is_active BOOLEAN DEFAULT TRUE,
    deactivated_at TIMESTAMP NULL,
    deactivated_by BIGINT NULL,
    created_by BIGINT NULL,
    updated_by BIGINT NULL,
    created_at TIMESTAMP,
    updated_at TIMESTAMP
);
```

**Tabel `auth_p_user_applications`:**
```sql
CREATE TABLE auth_p_user_applications (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    application_id BIGINT NOT NULL,
    granted_at TIMESTAMP,
    granted_by BIGINT NULL,
    UNIQUE(user_id, application_id)
);
```

### 25.3 Format Permission: `{app}.{module}.{action}`

```
Format: {app_code}.{module}.{action}

Contoh LIMS:
lims.sample.view
lims.sample.create
lims.sample.edit
lims.sample.delete
lims.sample.approve
lims.sample.release
lims.instrument.view
lims.instrument.calibrate
lims.report.generate
lims.report.sign_off

Contoh ERP:
erp.order.view
erp.order.create
erp.order.approve
erp.inventory.view
erp.inventory.adjust

Contoh HRIS:
hris.employee.view
hris.employee.create
hris.attendance.view
hris.payroll.process
```

**Kenapa 3 level?**
- `lims.sample.view` → Permission khusus app LIMS, module sample, aksi view
- Bisa filter: "tampilkan semua permission untuk app LIMS" → `WHERE permission LIKE 'lims.%'`
- Bisa filter: "user ini boleh apa di module sample?" → `WHERE permission LIKE 'lims.sample.%'`

### 25.4 Koneksi App ke Auth Database

**Setiap project Laravel (LIMS, ERP, HRIS) tambahkan config ini:**

#### A. Database Connection

```php
// config/database.php
'connections' => [
    // Database app sendiri (LIMS punya lims_db, ERP punya erp_db)
    'pgsql' => [
        'driver' => 'pgsql',
        'host' => env('DB_HOST', '127.0.0.1'),
        'port' => env('DB_PORT', '5432'),
        'database' => env('DB_DATABASE'),        // lims_db
        'username' => env('DB_USERNAME'),
        'password' => env('DB_PASSWORD'),
    ],

    // Koneksi ke auth database (SHARED)
    'auth' => [
        'driver' => 'pgsql',
        'host' => env('AUTH_DB_HOST', '127.0.0.1'),
        'port' => env('AUTH_DB_PORT', '5432'),
        'database' => env('AUTH_DB_DATABASE', 'auth_db'),
        'username' => env('AUTH_DB_USERNAME'),
        'password' => env('AUTH_DB_PASSWORD'),
    ],
],
```

#### B. User Model

```php
// Di SETIAP project Laravel
class User extends Authenticatable
{
    use HasRoles; // spatie/laravel-permission

    protected $connection = 'auth';          // ← Baca dari auth_db
    protected $table = 'auth_m_users';       // ← Tabel user terpusat
}
```

#### C. Session Config

```php
// config/session.php
'driver' => 'database',
'connection' => 'auth',                  // ← Simpan session di auth_db
'table' => 'auth_h_sessions',           // ← Tabel session yang sama
'domain' => env('SESSION_DOMAIN', '.domain.com'),  // ← Berlaku semua subdomain
'same_site' => 'lax',
'secure' => true,                        // ← HTTPS wajib di production
```

#### D. Environment Variables

```env
# === .env di setiap project ===

# Database app sendiri
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=lims_db           # atau erp_db, hris_db
DB_USERNAME=postgres
DB_PASSWORD=secret

# Database auth (SAMA di semua project)
AUTH_DB_HOST=127.0.0.1
AUTH_DB_PORT=5432
AUTH_DB_DATABASE=auth_db
AUTH_DB_USERNAME=postgres
AUTH_DB_PASSWORD=secret

# Session
SESSION_DRIVER=database
SESSION_DOMAIN=.domain.com

# App identifier (untuk filter permission)
APP_CODE=lims                 # atau 'erp', 'hris'
```

### 25.5 Alur SSO (Single Sign-On)

#### Web (Shared Session):

```
1. User buka lims.domain.com
2. Belum ada session → redirect ke auth.domain.com/login
3. User login di auth.domain.com
4. Session tersimpan di auth_db.auth_h_sessions
5. Cookie di-set untuk domain .domain.com
6. Redirect balik ke lims.domain.com
7. LIMS baca session dari auth_db → user sudah login ✅

8. User buka erp.domain.com
9. Browser kirim cookie yang sama (karena .domain.com)
10. ERP baca session dari auth_db → user sudah login ✅
11. Tidak perlu login lagi!
```

#### Mobile / Desktop (API Token):

```
1. App kirim POST auth.domain.com/api/login
   Body: { email, password }
2. Auth server validasi → return:
   {
     token: "abc123...",
     user: { id, name, email },
     permissions: ["lims.sample.view", "lims.sample.create", ...],
     applications: ["lims", "erp"]
   }
3. App simpan token di secure storage
4. Setiap API request:
   GET lims.domain.com/api/samples
   Header: Authorization: Bearer abc123...
5. LIMS validasi token di auth_db → OK ✅
```

### 25.6 Middleware: Cek Akses App

Setiap app Laravel perlu middleware untuk cek apakah user punya akses ke app ini:

```php
// app/Http/Middleware/EnsureUserHasAppAccess.php

class EnsureUserHasAppAccess
{
    public function handle(Request $request, Closure $next)
    {
        $user = $request->user();
        $appCode = config('app.code'); // 'lims', 'erp', dll

        // Cek apakah user punya akses ke app ini
        $hasAccess = DB::connection('auth')
            ->table('auth_p_user_applications as ua')
            ->join('auth_m_applications as a', 'a.id', '=', 'ua.application_id')
            ->where('ua.user_id', $user->id)
            ->where('a.code', $appCode)
            ->where('a.is_active', true)
            ->exists();

        if (!$hasAccess) {
            if ($request->wantsJson()) {
                return response()->json(['message' => 'No access to this application.'], 403);
            }
            abort(403, 'Anda tidak memiliki akses ke aplikasi ini.');
        }

        return $next($request);
    }
}
```

```php
// config/app.php — tambahkan di setiap project
'code' => env('APP_CODE', 'lims'),   // identifier app ini
```

### 25.7 Login Redirect Antar App

Di Auth Platform, setelah login bisa redirect ke app asal:

```php
// Auth Platform: LoginController
public function login(Request $request)
{
    // ... validasi credentials ...

    $redirectTo = $request->query('redirect', '/dashboard');

    // Kalau redirect ke app lain, pastikan domain valid
    if ($this->isValidAppUrl($redirectTo)) {
        return redirect($redirectTo);
    }

    return redirect('/dashboard');
}

private function isValidAppUrl(string $url): bool
{
    $allowedDomains = DB::table('auth_m_applications')
        ->where('is_active', true)
        ->pluck('url')
        ->toArray();

    $host = parse_url($url, PHP_URL_HOST);

    foreach ($allowedDomains as $domain) {
        if (str_contains($domain, $host)) {
            return true;
        }
    }

    return false;
}
```

Di setiap app, redirect ke auth kalau belum login:

```php
// Middleware di LIMS/ERP/HRIS
class RedirectIfNotAuthenticated
{
    public function handle(Request $request, Closure $next)
    {
        if (!auth()->check()) {
            $currentUrl = $request->fullUrl();
            return redirect("https://auth.domain.com/login?redirect={$currentUrl}");
        }
        return $next($request);
    }
}
```

### 25.8 Roadmap Implementasi

```
Phase 1: Auth Platform (minggu 1-2)
├── Buat project Laravel + React Inertia baru
├── Module: User, Role, Permission, Application
├── Login / Register / Forgot Password
├── Dashboard admin: kelola user, role, permission
├── API endpoint login (Sanctum) untuk mobile/desktop
└── Database: auth_db (PostgreSQL)

Phase 2: LIMS (minggu 3+)
├── Buat project Laravel + React Inertia baru
├── Config koneksi ke auth_db
├── User model pointing ke auth_db
├── Session shared via auth_db
├── Middleware: cek akses app
├── DDD modular monolith (arsitektur yang sudah kita buat)
└── Database: lims_db (PostgreSQL)

Phase 3: Hubungkan Project Lama (setelah LIMS stable)
├── Update config di ERP, HRIS, dll
├── Migrate tabel users lama ke auth_db
├── Update User model → $connection = 'auth'
├── Update session config
└── Test SSO antar app

Phase 4: Mobile & Desktop (future)
├── Mobile login via API token (Sanctum)
├── Desktop login via API token
└── Golang services login via API token
```

---

## 26. Deployment & Infrastructure

### 26.1 VPS Setup (Target)

```
VPS (Ubuntu 22.04+)
│
├── Nginx                    ← Reverse proxy
│   ├── auth.domain.com  →  Laravel Auth  (port 8000)
│   ├── lims.domain.com  →  Laravel LIMS  (port 8001)
│   ├── erp.domain.com   →  Laravel ERP   (port 8002)
│   └── hris.domain.com  →  Laravel HRIS  (port 8003)
│
├── PostgreSQL               ← Database server
│   ├── auth_db
│   ├── lims_db
│   ├── erp_db
│   └── hris_db
│
├── Redis (opsional)         ← Cache & queue
├── Supervisor               ← Kelola Laravel queue workers
└── SSL (Let's Encrypt)      ← HTTPS untuk semua domain
```

### 26.2 Nginx Config Per App

```nginx
# /etc/nginx/sites-available/auth.domain.com
server {
    listen 80;
    server_name auth.domain.com;
    root /var/www/auth/public;

    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

# Ulangi untuk lims.domain.com, erp.domain.com, dll
# Hanya ganti server_name dan root path
```

### 26.3 Folder Structure di Server

```
/var/www/
├── auth/                    ← Auth Platform
│   ├── public/
│   ├── .env
│   └── ...
├── lims/                    ← LIMS
│   ├── public/
│   ├── .env
│   └── ...
├── erp/                     ← ERP (project lama)
│   ├── public/
│   ├── .env
│   └── ...
└── hris/                    ← HRIS (project lama)
    ├── public/
    ├── .env
    └── ...
```

### 26.4 Deploy Checklist (Per App)

```bash
# 1. Pull latest code
cd /var/www/lims
git pull origin main

# 2. Install dependencies
composer install --no-dev --optimize-autoloader
npm install && npm run build

# 3. Run migrations
php artisan migrate --force

# 4. Clear & cache
php artisan config:cache
php artisan route:cache
php artisan view:cache

# 5. Restart queue worker (kalau ada)
sudo supervisorctl restart lims-worker:*

# 6. Done
```

### 26.5 Migrasi dari Shared Hosting ke VPS

| Step | Detail |
|---|---|
| 1. Setup VPS | Install Ubuntu, PHP, Nginx, PostgreSQL, Node.js |
| 2. Migrate DB dari MySQL ke PostgreSQL | Gunakan tool: `pgloader` atau export/import manual |
| 3. Upload semua project ke VPS | Git clone atau SCP |
| 4. Config Nginx per domain | 1 file config per subdomain |
| 5. Setup SSL | `certbot --nginx -d auth.domain.com -d lims.domain.com` |
| 6. Update DNS | Arahkan semua subdomain ke IP VPS |
| 7. Test semua app | Cek login, SSO, dan semua fitur |

## 27. Pagination Profiles (Cross App Standard)

Tujuan: semua app (`glims`, `gwin`, `gfin`) memakai standar page size yang konsisten berdasarkan jenis modul, bukan hardcode per halaman.

### 27.1 Matrix Standar

| Jenis Modul | Default | Opsi |
|---|---:|---|
| Master Data | 25 | 25, 50, 100 |
| Transaksi | 25 | 25, 50, 100 |
| Report | 50 | 50, 100, 250 |
| Audit | 50 | 50, 100, 250 |
| Mobile | 10 | 10, 25, 50 |

### 27.2 Frontend (Wajib Terpusat)

- Lokasi profile pusat: `packages/ui-shadcn/components/data-table/pagination-profiles.ts`
- Komponen utama tabel server-side: `packages/ui-shadcn/components/data-table/server-data-table.tsx`
- Aturan:
  - Jangan hardcode `pageSizes` di halaman kecuali kebutuhan khusus.
  - Default profile di-resolve dari route path (`master-data`, `transaction/transaksi`, `report`, `audit`, `mobile`).
  - Jika perlu override eksplisit, pakai prop `paginationProfile` pada `ServerDataTable`.

### 27.3 Backend (Wajib Terpusat)

- Lokasi helper per app:
  - `apps/glims/app/Modules/Shared/Infrastructure/Helpers/PaginationProfile.php`
  - `apps/gwin/app/Modules/Shared/Infrastructure/Helpers/PaginationProfile.php`
  - `apps/gfin/app/Modules/Shared/Infrastructure/Helpers/PaginationProfile.php`
- Aturan:
  - Semua fallback `per_page` wajib lewat helper profile, tidak boleh angka literal (`15`, `20`, `30`) di query/controller.
  - Helper harus melakukan:
    - resolve profile dari `pagination_profile` atau path route.
    - whitelist opsi sesuai matrix.
    - fallback ke default profile jika input `per_page` tidak valid.

### 27.4 Checklist Saat Menambah App/Module Baru

1. Daftarkan route/module prefix agar bisa dipetakan ke profile pagination.
2. Gunakan `ServerDataTable` tanpa hardcode `pageSizes` pada halaman baru.
3. Pastikan query backend menggunakan helper `PaginationProfile::resolvePerPage(...)`.
4. Verifikasi opsi yang tampil di UI sesuai matrix modul.
