# Coding Standards

Panduan ini mencerminkan pola yang **sudah dipakai** di codebase. Ikuti konvensi existing, jangan memperkenalkan pola baru tanpa alasan kuat.

---

## PHP & Laravel

### Versi & tooling

- PHP `^8.3`
- Laravel `^13.8`
- Formatter: Laravel Pint (`./vendor/bin/pint`) — jalankan sebelum commit jika mengubah banyak file PHP
- Test: PHPUnit 12, `php artisan test`

### Struktur controller

```php
class XxxController extends Controller
{
    public function __construct(private XxxService $service) {}

    public function index(Request $request)
    {
        // Query dengan when(), paginate(), withQueryString()
        return view('xxx.index', compact('...'));
    }

    public function store(Request $request)
    {
        $validated = $request->validate([...]);
        $this->service->simpan(...);
        return redirect()->route('xxx.index')->with('success', '...');
    }
}
```

### Validasi

- **Inline** di controller via `$request->validate()` — jangan buat Form Request kecuali diminta.
- Gunakan rule `exists:master_*,id` untuk FK ke tabel master.
- Enum values dalam rule `in:` harus match persis dengan nilai di database (case-sensitive).

### Service layer

- Semua operasi multi-tabel **wajib** dibungkus `DB::transaction()`.
- Throw `RuntimeException` untuk error bisnis (stok tidak cukup) — tangkap di controller, return `back()->withInput()->withErrors()`.
- Inject dependency via constructor (`StokService`, `KasService`).
- Jangan inject `Request` ke service.

### Model

```php
class Obat extends Model
{
    protected $table = 'master_obat';  // Wajib jika nama tabel tidak plural Inggris

    protected $fillable = [...];

    protected function casts(): array
    {
        return [
            'harga_otc' => 'decimal:2',
            'tanggal_expired' => 'date',
        ];
    }

    public function cabang(): BelongsTo { ... }
}
```

- Gunakan `casts()` method (Laravel 11+ style), bukan `protected $casts`.
- Relasi didefinisikan eksplisit dengan return type.
- Dictionary models: gunakan trait `IsMasterDictionary`.

### Query patterns

```php
// Filter opsional
Model::query()
    ->when($request->get('q'), fn ($q, $search) => $q->where(...))
    ->with(['relation'])
    ->latest()
    ->paginate(10)
    ->withQueryString();
```

### Error handling

| Situasi | Pola |
|---------|------|
| Validasi input gagal | Laravel auto redirect + errors |
| Stok tidak cukup | `RuntimeException` → catch di controller |
| Record tidak ditemukan | `findOrFail()` / route model binding |

Jangan over-engineer try/catch global.

---

## Blade & frontend

### Layout

```blade
@extends('layouts.app')

@section('title', 'Judul Tab')
@section('page-title', 'Judul Halaman')

@section('content')
    ...
@endsection
```

### Komponen

Prefer anonymous components yang sudah ada:

```blade
<x-button variant="primary" href="{{ route('obat.create') }}">Tambah</x-button>
<x-card title="Filter">...</x-card>
<x-form-field label="Nama" name="nama" :value="old('nama')" required />
<x-badge variant="success">Lunas</x-badge>
```

### Flash messages

```php
return redirect()->route('...')->with('success', 'Pesan sukses Bahasa Indonesia.');
```

Ditampilkan otomatis via `<x-alert>` di layout.

### Form dinamis (transaksi)

- Gunakan Alpine.js `x-data` dengan array `rows`.
- Load Alpine via CDN di `@push('scripts')`.
- Gunakan `<x-searchable-select-alpine>` untuk dropdown obat di baris dinamis.
- Panggil `window.SiagaSearchableSelect.initWithin(container)` setelah tambah baris.

### Styling

- Prefer class `siaga-*` dari `app.css` untuk konsistensi.
- Beberapa form transaksi masih memakai inline Tailwind (`bg-white rounded-xl border border-slate-200`) — acceptable, jangan refactor massal tanpa diminta.
- Warna semantic: `text-success`, `bg-danger-soft`, dll. dari `@theme` di `app.css`.

---

## JavaScript

- ES modules via Vite (`resources/js/`).
- Export functions dari `searchable-select.js`, expose ke `window.SiagaSearchableSelect` jika perlu dari Alpine.
- Sidebar logic di `app.js` — jangan duplikasi.
- Hindari framework JS selain Alpine untuk form dinamis.

---

## Database & migration

```php
Schema::create('nama_tabel', function (Blueprint $table) {
    $table->id();
    $table->foreignId('cabang_id')->constrained('master_cabang');
    $table->decimal('harga', 15, 2)->default(0);
    $table->timestamps();
});
```

- Nama tabel: snake_case Bahasa Indonesia.
- Decimal uang: `decimal(15, 2)`.
- FK selalu ke nama tabel eksplisit (`master_cabang`, bukan `cabang`).
- Buat migration baru untuk perubahan skema — jangan edit migration lama yang sudah di-deploy.
- Uji di SQLite (test) dan MySQL (dev).

### Seeder

- Gunakan `updateOrInsert` by `kode` untuk dictionary (idempotent).
- Data dummy realistis, bukan data real pelanggan.

---

## Testing

```php
class XxxTest extends TestCase
{
    use RefreshDatabase;

    public function test_something(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user)->post('/path', [...])
            ->assertRedirect('/expected');
        $this->assertDatabaseHas('table', [...]);
    }
}
```

- Feature tests untuk flow HTTP + database.
- Buat model langsung di test (`Cabang::create([...])`) — tidak perlu factory untuk semua model.
- PHPUnit env: SQLite in-memory (lihat `phpunit.xml`).

**Prioritas test baru:**

1. Penjualan + FEFO
2. KasService / pembayaran tagihan
3. Koreksi stok
4. StokService unit tests

---

## Git & commit

- Branch agent: `cursor/<deskripsi>-7b6f`
- Commit message deskriptif Bahasa Indonesia atau Inggris — konsisten per commit.
- Commit kecil per modul/fitur.
- Jangan commit `.env`, `vendor/`, `node_modules/`.

---

## Yang harus dihindari (V0)

| Jangan | Alasan |
|--------|--------|
| Form Request classes | Belum dipakai, tambah kompleksitas |
| Policies / Gates | Belum ada multi-role |
| Events / Jobs / Queues | Belum ada async work |
| API routes | Out of scope V0 |
| Repository pattern | Service + Eloquent cukup |
| Helper global functions | Gunakan service class |
| Over-abstraction | Satu method helper untuk 1-2 baris |

---

## Dependency injection

Laravel auto-resolve constructor type-hints. `AppServiceProvider` kosong — tidak perlu binding manual kecuali ada interface.

```php
// OK — auto-resolve
public function __construct(private PembelianService $pembelianService) {}

// OK — resolve dari model
public function getStokTotal(): int
{
    return app(StokService::class)->getStokTotal(null, $this->id);
}
```

---

## Git & pull request

- **Base kebenaran:** `main`. Selalu `git checkout main && git pull` sebelum branch baru.
- **Nama branch:** `feat/<modul>-<tujuan>` (lihat `naming-conventions.md`).
- **Satu PR = satu tujuan.** Setelah merge, hapus branch lokal + remote.
- **Jangan** merge feature↔feature tanpa sync `main` dulu.
- Saat conflict: prioritaskan helper bersama di model (`User` role methods) dan shared view (`penjualan._form`, komponen cabang aktif). Jangan buang method/view dari `main` hanya karena file “juga berubah” di feature branch.
- Setelah merge besar yang menyentuh auth/form transaksi, smoke-test login + satu create penjualan OTC/Medis.
