# Rencana Penambahan Fitur Payroll - SHF HRIS

> Analisis dan best practice implementasi modul payroll berdasarkan arsitektur eksisting aplikasi.

---

## Daftar Isi

1. [Analisis Kondisi Eksisting](#1-analisis-kondisi-eksisting)
2. [Ruang Lingkup Fitur Payroll](#2-ruang-lingkup-fitur-payroll)
3. [Desain Database](#3-desain-database)
4. [Arsitektur Implementasi](#4-arsitektur-implementasi)
5. [Alur Proses Payroll](#5-alur-proses-payroll)
6. [Struktur File & Kode](#6-struktur-file--kode)
7. [Best Practices Sesuai Pola Eksisting](#7-best-practices-sesuai-pola-eksisting)
8. [Urutan Implementasi](#8-urutan-implementasi)
9. [Risiko & Mitigasi](#9-risiko--mitigasi)

---

## 1. Analisis Kondisi Eksisting

### 1.1 Stack Teknologi
| Layer | Teknologi |
|-------|-----------|
| Backend | Laravel 7.0 (PHP) |
| Frontend | Vue.js 2.6 + Inertia.js |
| UI Library | BootstrapVue 4.5 |
| Database | MySQL (dual: `mysql` + `opis_db`) |
| Auth | Session-based (web) + JWT (mobile API) |
| PDF | barryvdh/laravel-dompdf |
| Excel | maatwebsite/excel v3.1 |
| Realtime | Pusher |

### 1.2 Data yang Sudah Tersedia untuk Payroll
Data berikut sudah ada di sistem dan dapat langsung diintegrasikan ke kalkulasi payroll:

| Sumber Data | Tabel/Model | Kegunaan untuk Payroll |
|-------------|-------------|----------------------|
| Kehadiran | `d_employee_attendances` | Hitung hari kerja aktual, keterlambatan |
| Lembur | `d_overtimes` + `t_employee_submissions` | Hitung upah lembur |
| Cuti | `d_leaves` + `d_employee_leave_quotas` | Cuti di luar tanggungan → potong gaji |
| Izin | `d_permits` | Potong gaji jika izin tidak berbayar |
| Surat Peringatan | `d_employee_warning_letters` | Potongan sanksi |
| Hari Libur | `ref_holidays` | Kecualikan dari kalkulasi hari kerja |
| Jam Kerja | `ref_working_hours` | Standar jam kerja untuk lembur |
| Karyawan | `m_employees` + `d_employee` | Data pokok karyawan |
| Jabatan | `positions` + `emp_history` | Komponen gaji berdasarkan jabatan |
| Cabang | `m_branches` | Struktur gaji per cabang |

### 1.3 Gap Analysis
Yang **belum ada** dan perlu dibuat:

- [ ] Tabel komponen gaji (gaji pokok, tunjangan, potongan)
- [ ] Tabel PTKP & tarif pajak PPh 21
- [ ] Tabel BPJS (Kesehatan + Ketenagakerjaan)
- [ ] Periode penggajian (payroll period)
- [ ] Run payroll (batch kalkulasi)
- [ ] Slip gaji per karyawan
- [ ] Approval payroll
- [ ] Rekap payroll (laporan)
- [ ] Export ke Excel/PDF

---

## 2. Ruang Lingkup Fitur Payroll

### 2.1 Komponen Penghasilan (Earnings)
```
+ Gaji Pokok
+ Tunjangan Jabatan
+ Tunjangan Transport
+ Tunjangan Makan
+ Tunjangan Komunikasi
+ Tunjangan Perumahan
+ Upah Lembur
+ Tunjangan Lainnya (konfigurabel)
```

### 2.2 Komponen Potongan (Deductions)
```
- PPh 21 (PTKP-based, tarif progresif)
- BPJS Kesehatan (4% perusahaan / 1% karyawan)
- BPJS Ketenagakerjaan JHT (3.7% perusahaan / 2% karyawan)
- BPJS Ketenagakerjaan JP (2% perusahaan / 1% karyawan)
- Potongan Keterlambatan
- Potongan Absensi (tidak hadir tanpa keterangan)
- Potongan Pinjaman/Cicilan
- Potongan Lainnya (konfigurabel)
```

### 2.3 Formula Utama
```
Gaji Bersih = Total Penghasilan - Total Potongan

PPh 21 = f(Penghasilan Bruto Setahun - PTKP) × Tarif Progresif
BPJS   = Persentase × Dasar Pengenaan Upah (DPU)
Lembur = (Gaji Pokok / 173) × Multiplier × Jam Lembur
```

---

## 3. Desain Database

### 3.1 Tabel Referensi (Master Data)

#### `ref_payroll_components` — Komponen Gaji
```sql
CREATE TABLE ref_payroll_components (
    id            BIGINT PRIMARY KEY AUTO_INCREMENT,
    code          VARCHAR(20) UNIQUE NOT NULL,   -- e.g. BASIC, TRANSP, MEAL
    name          VARCHAR(100) NOT NULL,
    type          ENUM('earning', 'deduction') NOT NULL,
    calc_type     ENUM('fixed', 'percentage', 'formula') NOT NULL,
    is_taxable    TINYINT(1) DEFAULT 1,          -- masuk objek PPh 21?
    is_bpjs_base  TINYINT(1) DEFAULT 0,          -- masuk DPU BPJS?
    is_active     TINYINT(1) DEFAULT 1,
    sort_order    INT DEFAULT 0,
    created_at    TIMESTAMP,
    updated_at    TIMESTAMP
);
```

#### `ref_ptkp` — PTKP PPh 21
```sql
CREATE TABLE ref_ptkp (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    code        VARCHAR(10) NOT NULL,  -- TK/0, TK/1, K/0, K/1, K/2, K/3
    description VARCHAR(100) NOT NULL,
    annual_amount DECIMAL(15,2) NOT NULL,
    effective_date DATE NOT NULL,
    created_at  TIMESTAMP,
    updated_at  TIMESTAMP
);
```

#### `ref_pph21_rates` — Tarif Pajak Progresif
```sql
CREATE TABLE ref_pph21_rates (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    income_from     DECIMAL(15,2) NOT NULL,
    income_to       DECIMAL(15,2),               -- NULL = tidak terbatas
    rate_percentage DECIMAL(5,2) NOT NULL,
    effective_date  DATE NOT NULL,
    created_at      TIMESTAMP,
    updated_at      TIMESTAMP
);
```

#### `ref_bpjs_rates` — Tarif BPJS
```sql
CREATE TABLE ref_bpjs_rates (
    id                   BIGINT PRIMARY KEY AUTO_INCREMENT,
    type                 ENUM('kesehatan', 'jht', 'jp', 'jkk', 'jkm') NOT NULL,
    employer_rate        DECIMAL(5,2) NOT NULL,
    employee_rate        DECIMAL(5,2) NOT NULL,
    max_wage_base        DECIMAL(15,2),           -- batas atas DPU
    effective_date       DATE NOT NULL,
    created_at           TIMESTAMP,
    updated_at           TIMESTAMP
);
```

#### `ref_payroll_periods` — Periode Penggajian
```sql
CREATE TABLE ref_payroll_periods (
    id            BIGINT PRIMARY KEY AUTO_INCREMENT,
    year          SMALLINT NOT NULL,
    month         TINYINT NOT NULL,              -- 1–12
    start_date    DATE NOT NULL,
    end_date      DATE NOT NULL,
    pay_date      DATE,                          -- tanggal transfer gaji
    status        ENUM('open','locked','paid') DEFAULT 'open',
    created_by    BIGINT,
    locked_at     TIMESTAMP NULL,
    paid_at       TIMESTAMP NULL,
    created_at    TIMESTAMP,
    updated_at    TIMESTAMP,
    UNIQUE KEY uq_year_month (year, month)
);
```

### 3.2 Tabel Konfigurasi Gaji Karyawan

#### `d_employee_salary` — Struktur Gaji Pokok per Karyawan
```sql
CREATE TABLE d_employee_salary (
    id             BIGINT PRIMARY KEY AUTO_INCREMENT,
    id_employee    VARCHAR(20) NOT NULL,          -- FK ke m_employees
    component_id   BIGINT NOT NULL,               -- FK ke ref_payroll_components
    amount         DECIMAL(15,2) NOT NULL,
    effective_date DATE NOT NULL,
    end_date       DATE,                          -- NULL = masih berlaku
    ptkp_code      VARCHAR(10),                   -- FK ke ref_ptkp (hanya untuk salary utama)
    created_by     BIGINT,
    created_at     TIMESTAMP,
    updated_at     TIMESTAMP
);
```

### 3.3 Tabel Transaksi Payroll

#### `t_payroll_runs` — Batch Run Payroll
```sql
CREATE TABLE t_payroll_runs (
    id                BIGINT PRIMARY KEY AUTO_INCREMENT,
    period_id         BIGINT NOT NULL,             -- FK ke ref_payroll_periods
    id_branch         BIGINT,                      -- NULL = semua cabang
    status            ENUM('draft','calculated','approved','paid') DEFAULT 'draft',
    total_gross       DECIMAL(15,2) DEFAULT 0,
    total_deduction   DECIMAL(15,2) DEFAULT 0,
    total_net         DECIMAL(15,2) DEFAULT 0,
    employee_count    INT DEFAULT 0,
    notes             TEXT,
    calculated_at     TIMESTAMP NULL,
    approved_by       BIGINT,
    approved_at       TIMESTAMP NULL,
    created_by        BIGINT,
    created_at        TIMESTAMP,
    updated_at        TIMESTAMP
);
```

#### `d_payroll_slips` — Slip Gaji per Karyawan
```sql
CREATE TABLE d_payroll_slips (
    id                BIGINT PRIMARY KEY AUTO_INCREMENT,
    run_id            BIGINT NOT NULL,             -- FK ke t_payroll_runs
    period_id         BIGINT NOT NULL,
    id_employee       VARCHAR(20) NOT NULL,
    working_days      INT DEFAULT 0,               -- hari kerja standar bulan ini
    actual_work_days  INT DEFAULT 0,               -- hari hadir aktual
    overtime_hours    DECIMAL(6,2) DEFAULT 0,
    late_minutes      INT DEFAULT 0,
    total_gross       DECIMAL(15,2) DEFAULT 0,
    total_deduction   DECIMAL(15,2) DEFAULT 0,
    total_net         DECIMAL(15,2) DEFAULT 0,
    pph21_annual_base DECIMAL(15,2) DEFAULT 0,     -- PKP setahun
    pph21_amount      DECIMAL(15,2) DEFAULT 0,
    status            ENUM('draft','approved','paid') DEFAULT 'draft',
    paid_at           TIMESTAMP NULL,
    created_at        TIMESTAMP,
    updated_at        TIMESTAMP
);
```

#### `d_payroll_slip_details` — Rincian Komponen per Slip
```sql
CREATE TABLE d_payroll_slip_details (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    slip_id         BIGINT NOT NULL,              -- FK ke d_payroll_slips
    component_id    BIGINT NOT NULL,
    component_name  VARCHAR(100) NOT NULL,        -- snapshot nama saat kalkulasi
    type            ENUM('earning','deduction') NOT NULL,
    amount          DECIMAL(15,2) NOT NULL,
    notes           VARCHAR(255),
    created_at      TIMESTAMP,
    updated_at      TIMESTAMP
);
```

#### `d_employee_loans` — Pinjaman Karyawan (opsional)
```sql
CREATE TABLE d_employee_loans (
    id               BIGINT PRIMARY KEY AUTO_INCREMENT,
    id_employee      VARCHAR(20) NOT NULL,
    amount           DECIMAL(15,2) NOT NULL,
    installment      DECIMAL(15,2) NOT NULL,     -- cicilan per bulan
    remaining        DECIMAL(15,2) NOT NULL,
    start_period     INT,                         -- FK ke ref_payroll_periods
    status           ENUM('active','settled') DEFAULT 'active',
    created_by       BIGINT,
    created_at       TIMESTAMP,
    updated_at       TIMESTAMP
);
```

---

## 4. Arsitektur Implementasi

### 4.1 Struktur Controllers (ikuti pola eksisting)

```
app/Http/Controllers/
├── PayrollPeriodController.php       -- CRUD periode penggajian
├── PayrollComponentController.php    -- CRUD komponen gaji (master)
├── EmployeeSalaryController.php      -- Struktur gaji per karyawan
├── PayrollRunController.php          -- Run, kalkulasi, approve payroll
├── PayrollSlipController.php         -- Lihat slip gaji
├── EmployeeLoanController.php        -- Pinjaman karyawan
└── PayrollReportController.php       -- Rekap & laporan payroll
```

### 4.2 Struktur Models

```
app/Models/
├── RefPayrollPeriod.php
├── RefPayrollComponent.php
├── RefPtkp.php
├── RefPph21Rate.php
├── RefBpjsRate.php
├── DEmployeeSalary.php
├── TPayrollRun.php
├── DPayrollSlip.php
├── DPayrollSlipDetail.php
└── DEmployeeLoan.php
```

### 4.3 Service Layer (BARU — rekomendasi untuk logika kompleks)

Karena kalkulasi payroll kompleks, gunakan **Service class** terpisah dari Controller:

```
app/Services/Payroll/
├── PayrollCalculatorService.php      -- Orkestrator utama kalkulasi
├── AttendanceAggregatorService.php   -- Agregasi data kehadiran per periode
├── OvertimeCalculatorService.php     -- Hitung upah lembur
├── Pph21CalculatorService.php        -- Hitung PPh 21
├── BpjsCalculatorService.php         -- Hitung iuran BPJS
├── DeductionCalculatorService.php    -- Potongan (terlambat, absen, pinjaman)
└── PayrollSlipGeneratorService.php   -- Generate PDF slip gaji
```

**Contoh interface `PayrollCalculatorService`:**
```php
class PayrollCalculatorService
{
    public function calculate(TPayrollRun $run): TPayrollRun
    {
        $employees = $this->getEligibleEmployees($run);

        foreach ($employees as $employee) {
            $slip = $this->calculateSlip($employee, $run->period);
            $slip->save();
        }

        $run->updateTotals();
        return $run;
    }

    private function calculateSlip(Employee $employee, RefPayrollPeriod $period): DPayrollSlip
    {
        $attendance = $this->attendanceAggregator->aggregate($employee, $period);
        $earnings   = $this->calculateEarnings($employee, $attendance);
        $deductions = $this->calculateDeductions($employee, $attendance, $earnings);

        return DPayrollSlip::create([
            'total_gross'      => $earnings->sum(),
            'total_deduction'  => $deductions->sum(),
            'total_net'        => $earnings->sum() - $deductions->sum(),
        ]);
    }
}
```

### 4.4 Events & Listeners (ikuti pola eksisting)

```
app/Events/
├── OnPayrollRunCreated.php
├── OnPayrollRunCalculated.php
├── OnPayrollRunApproved.php
└── OnPayrollSlipIssued.php

app/Listeners/
├── SendPayrollApprovalNotification.php    -- notif ke approver
├── SendPayslipEmailToEmployee.php         -- kirim slip ke email karyawan
└── LogPayrollActivity.php                 -- catat di activity log
```

### 4.5 Policies (ikuti pola 35 policies eksisting)

```
app/Policies/
├── PayrollRunPolicy.php     -- view, calculate, approve, pay
├── PayrollSlipPolicy.php    -- view (karyawan hanya lihat milik sendiri)
└── EmployeeSalaryPolicy.php -- view, create, update (HR/Admin only)
```

---

## 5. Alur Proses Payroll

```
[1. Setup Master Data]
    Definisi komponen gaji → PTKP & tarif PPh 21 → Tarif BPJS
    Struktur gaji per karyawan (EmployeeSalary)

[2. Buka Periode]
    Admin buat RefPayrollPeriod (bulan/tahun, tanggal mulai-akhir)
    Status: open

[3. Run Payroll]
    Admin trigger "Hitung Gaji" → TPayrollRun (status: draft)
    PayrollCalculatorService::calculate() dipanggil:
      ├── Ambil data kehadiran dari d_employee_attendances
      ├── Ambil lembur approved dari d_overtimes
      ├── Ambil cuti/izin dari d_leaves, d_permits
      ├── Kalkulasi earning per komponen
      ├── Kalkulasi PPh 21 (metode gross/nett sesuai setting)
      ├── Kalkulasi BPJS
      ├── Kalkulasi potongan lain
      └── Simpan ke d_payroll_slips + d_payroll_slip_details
    Status: calculated

[4. Review & Koreksi]
    HR review setiap slip gaji
    Koreksi manual jika diperlukan (catat alasan)

[5. Approval]
    Kirim ke approver (Finance Manager / Direktur)
    OnPayrollRunApproved → event dipicu
    Status: approved

[6. Pembayaran]
    Export ke format transfer bank (CSV/Excel)
    Mark as paid → tanggal bayar dicatat
    Status: paid

[7. Distribusi Slip]
    Karyawan dapat lihat slip di portal self-service
    Opsional: kirim email PDF slip gaji
```

---

## 6. Struktur File & Kode

### 6.1 Routes — tambahkan di `routes/web.php`

```php
// Payroll - Admin
Route::prefix('payroll')->name('payroll.')->middleware(['auth'])->group(function () {

    // Master Data
    Route::resource('components', PayrollComponentController::class);
    Route::resource('ptkp', PtkpController::class);
    Route::resource('bpjs-rates', BpjsRateController::class);

    // Periode
    Route::resource('periods', PayrollPeriodController::class);
    Route::post('periods/{period}/lock', [PayrollPeriodController::class, 'lock'])
         ->name('periods.lock');

    // Run Payroll
    Route::resource('runs', PayrollRunController::class);
    Route::post('runs/{run}/calculate', [PayrollRunController::class, 'calculate'])
         ->name('runs.calculate');
    Route::post('runs/{run}/approve', [PayrollRunController::class, 'approve'])
         ->name('runs.approve');
    Route::post('runs/{run}/mark-paid', [PayrollRunController::class, 'markPaid'])
         ->name('runs.markPaid');

    // Slip Gaji
    Route::get('slips', [PayrollSlipController::class, 'index'])->name('slips.index');
    Route::get('slips/{slip}', [PayrollSlipController::class, 'show'])->name('slips.show');
    Route::get('slips/{slip}/pdf', [PayrollSlipController::class, 'downloadPdf'])
         ->name('slips.pdf');

    // Struktur Gaji Karyawan
    Route::resource('employee-salary', EmployeeSalaryController::class);

    // Laporan
    Route::get('reports', [PayrollReportController::class, 'index'])->name('reports.index');
    Route::get('reports/export', [PayrollReportController::class, 'export'])
         ->name('reports.export');
});

// Self-service: Karyawan lihat slip gaji sendiri
Route::prefix('my/payslip')->name('my.payslip.')->middleware(['auth'])->group(function () {
    Route::get('/', [PayrollSlipController::class, 'mySlips'])->name('index');
    Route::get('/{slip}', [PayrollSlipController::class, 'mySlipDetail'])->name('show');
    Route::get('/{slip}/pdf', [PayrollSlipController::class, 'mySlipPdf'])->name('pdf');
});
```

### 6.2 API Routes — tambahkan di `routes/api.php`

```php
// Mobile API - Slip gaji
Route::prefix('payslip')->middleware('auth:api_mobile')->group(function () {
    Route::get('/', [PayrollSlipController::class, 'serviceProfileGet']);
    Route::get('/{id}', [PayrollSlipController::class, 'serviceProfileShow']);
});
```

### 6.3 Contoh Controller — `PayrollRunController`

```php
class PayrollRunController extends Controller
{
    protected PayrollCalculatorService $calculator;

    public function __construct(PayrollCalculatorService $calculator)
    {
        $this->calculator = $calculator;
    }

    public function index()
    {
        $this->authorize('viewAny', TPayrollRun::class);

        return Inertia::render('Payroll/RunIndex', [
            'runs' => TPayrollRun::with('period')
                        ->orderByDesc('created_at')
                        ->paginate(15),
            '__route_name' => 'payroll.runs.index',
        ]);
    }

    public function calculate(TPayrollRun $run)
    {
        $this->authorize('calculate', $run);

        try {
            DB::transaction(function () use ($run) {
                $this->calculator->calculate($run);
            });

            return redirect()->route('payroll.runs.show', $run)
                ->with('success', 'Kalkulasi payroll berhasil.');
        } catch (\Exception $e) {
            return back()->with('error', 'Kalkulasi gagal: ' . $e->getMessage());
        }
    }
}
```

### 6.4 Vue Pages — tambahkan di `resources/js/Pages/`

```
resources/js/Pages/Payroll/
├── RunIndex.vue          -- Daftar payroll run
├── RunShow.vue           -- Detail run + daftar slip
├── SlipShow.vue          -- Detail slip satu karyawan
├── ComponentIndex.vue    -- Master komponen gaji
├── PeriodIndex.vue       -- Periode penggajian
└── MyPayslip/
    ├── Index.vue         -- Daftar slip karyawan (self-service)
    └── Show.vue          -- Detail slip karyawan
```

---

## 7. Best Practices Sesuai Pola Eksisting

### 7.1 Ikuti Pola `t_employee_submissions`
Gunakan status lifecycle yang konsisten:

```
Payroll Run: draft → calculated → approved → paid
Slip Gaji:   draft → approved → paid
```

### 7.2 Snapshot Data Saat Kalkulasi
**PENTING:** Simpan snapshot nilai saat kalkulasi di `d_payroll_slip_details.amount` dan `component_name`. Jangan referensikan langsung ke master data karena nilai bisa berubah di masa depan.

```php
// BENAR: Snapshot
DPayrollSlipDetail::create([
    'component_id'   => $component->id,
    'component_name' => $component->name,  // snapshot
    'amount'         => $calculatedAmount, // hasil kalkulasi, bukan dari master
]);
```

### 7.3 Gunakan Database Transaction
Seluruh kalkulasi satu run payroll harus atomic:

```php
DB::transaction(function () use ($run) {
    $run->slips()->delete(); // hapus draft sebelumnya jika re-calculate
    foreach ($employees as $employee) {
        $this->createSlip($employee, $run);
    }
    $run->update(['status' => 'calculated', 'calculated_at' => now()]);
});
```

### 7.4 Lock Periode Setelah Paid
Cegah modifikasi data kehadiran/lembur setelah payroll dibayar:

```php
// Di AttendanceController::update()
$period = RefPayrollPeriod::findForDate($attendance->date);
if ($period && $period->status === 'paid') {
    abort(403, 'Tidak dapat mengubah data pada periode yang sudah dibayarkan.');
}
```

### 7.5 Pisahkan Logika Bisnis ke Service
Jangan taruh kalkulasi di Controller. Gunakan `PayrollCalculatorService`:

```php
// Controller hanya orkestrasi
public function calculate(TPayrollRun $run)
{
    $this->authorize('calculate', $run);
    DB::transaction(fn() => $this->calculatorService->calculate($run));
    return redirect()->back()->with('success', 'Berhasil dihitung.');
}
```

### 7.6 Gunakan Events untuk Side Effects
Ikuti pola `OnDLeaveStored` / `OnDOvertimeStored`:

```php
// Di PayrollRunController::approve()
event(new OnPayrollRunApproved($run));

// Listener otomatis handle: kirim notif ke HR, log aktivitas, dll.
```

### 7.7 Activity Logging
Catat semua aksi kritis ke activity log (sudah ada Spatie):

```php
activity()
    ->causedBy(auth()->user())
    ->performedOn($run)
    ->withProperties(['period' => $run->period->name])
    ->log('Payroll run approved');
```

### 7.8 Policy untuk Otorisasi
Ikuti 35 policies eksisting. Contoh `PayrollRunPolicy`:

```php
class PayrollRunPolicy
{
    public function viewAny(User $user): bool
    {
        return in_array($user->role, ['admin', 'hr', 'finance']);
    }

    public function calculate(User $user, TPayrollRun $run): bool
    {
        return $user->role === 'hr' && $run->status === 'draft';
    }

    public function approve(User $user, TPayrollRun $run): bool
    {
        return in_array($user->role, ['admin', 'finance'])
            && $run->status === 'calculated';
    }
}
```

### 7.9 Export PDF Menggunakan DomPDF yang Sudah Ada

```php
// Di PayrollSlipController::downloadPdf()
$slip = DPayrollSlip::with(['details', 'employee', 'period'])->findOrFail($id);
$pdf  = PDF::loadView('pdf.payslip', compact('slip'));
return $pdf->download("slip-gaji-{$slip->employee->name}-{$slip->period->name}.pdf");
```

Buat template blade di `resources/views/pdf/payslip.blade.php`.

### 7.10 Export Excel Menggunakan Maatwebsite yang Sudah Ada

```php
// app/Exports/PayrollExport.php
class PayrollExport implements FromCollection, WithHeadings
{
    public function collection(): Collection
    {
        return DPayrollSlip::with('employee', 'period')
            ->where('run_id', $this->runId)
            ->get();
    }
}
```

---

## 8. Urutan Implementasi

### Phase 1 — Fondasi (Sprint 1-2)
- [ ] Migrasi database: semua tabel referensi dan master
- [ ] Seeder: PTKP 2024, tarif PPh 21, tarif BPJS
- [ ] Model Eloquent + relationships
- [ ] CRUD `RefPayrollComponent` (komponen gaji)
- [ ] CRUD `DEmployeeSalary` (struktur gaji karyawan)
- [ ] CRUD `RefPayrollPeriod` (periode penggajian)

### Phase 2 — Kalkulasi (Sprint 3-4)
- [ ] `AttendanceAggregatorService` — agregasi kehadiran per periode
- [ ] `OvertimeCalculatorService` — kalkulasi lembur
- [ ] `Pph21CalculatorService` — kalkulasi PPh 21
- [ ] `BpjsCalculatorService` — kalkulasi BPJS
- [ ] `DeductionCalculatorService` — potongan lain
- [ ] `PayrollCalculatorService` — orkestrator utama
- [ ] Unit test untuk setiap service kalkulasi

### Phase 3 — Workflow & UI (Sprint 5-6)
- [ ] `TPayrollRun` CRUD + kalkulasi trigger
- [ ] `DPayrollSlip` view + review + koreksi manual
- [ ] Approval workflow (ikuti pola `DSubmissionApproval`)
- [ ] Vue pages: RunIndex, RunShow, SlipShow
- [ ] Policy + otorisasi

### Phase 4 — Output (Sprint 7-8)
- [ ] PDF slip gaji (template blade + DomPDF)
- [ ] Export Excel rekap payroll
- [ ] Email notifikasi slip gaji ke karyawan
- [ ] Self-service portal karyawan (My Payslip)
- [ ] Mobile API endpoint slip gaji

### Phase 5 — Laporan & Polish (Sprint 9)
- [ ] Laporan rekap per periode, per cabang, per departemen
- [ ] Dashboard payroll summary
- [ ] Lock periode otomatis setelah paid
- [ ] Audit log / history perubahan gaji

---

## 9. Risiko & Mitigasi

| Risiko | Dampak | Mitigasi |
|--------|--------|----------|
| Kalkulasi PPh 21 salah | Denda pajak, komplain karyawan | Unit test komprehensif + validasi manual periode pertama |
| Data kehadiran tidak konsisten | Slip gaji salah | Validasi data sebelum run: cek gap kehadiran, overtime status |
| Re-kalkulasi setelah approved | Data tidak konsisten | Lock run setelah approved, hanya admin yang bisa override |
| Perubahan tarif PTKP/BPJS | Semua kalkulasi mendatang salah | Tabel referensi dengan `effective_date`, ambil tarif sesuai tanggal periode |
| Performa lambat untuk banyak karyawan | Timeout pada proses kalkulasi | Gunakan `Queue` + `Job` untuk kalkulasi batch (dispatch ke background) |
| Gaji karyawan bocor ke sesama karyawan | Masalah privasi/hukum | Policy ketat: karyawan hanya bisa lihat slip milik sendiri |

### 9.1 Rekomendasi Queue untuk Kalkulasi Batch

```php
// Gunakan Job untuk proses background
class CalculatePayrollJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function handle(PayrollCalculatorService $calculator): void
    {
        $calculator->calculate($this->run);
        $this->run->update(['status' => 'calculated']);

        // Kirim notifikasi ke HR bahwa kalkulasi selesai
        event(new OnPayrollRunCalculated($this->run));
    }
}

// Dispatch dari controller
CalculatePayrollJob::dispatch($run);
```

---

## Referensi Regulasi Indonesia

| Regulasi | Keterangan |
|----------|------------|
| UU PPh Pasal 21 | Pajak penghasilan karyawan, tarif progresif |
| PP 36/2021 jo PP 51/2023 | PTKP terbaru |
| PP 44/2015 jo PP 60/2015 | BPJS Ketenagakerjaan (JHT, JP, JKK, JKM) |
| Perpres 75/2019 | BPJS Kesehatan |
| UU Cipta Kerja (UU 11/2020) | Formula upah lembur |
| PP 35/2021 | Aturan lembur: max 4 jam/hari, 18 jam/minggu |

> **Catatan:** Selalu verifikasi tarif pajak dan BPJS terkini sebelum implementasi karena regulasi dapat berubah setiap tahun.

---

*Dokumen ini dibuat berdasarkan analisis codebase SHF HRIS pada 2026-04-13.*
*Framework: Laravel 7 + Vue.js 2.6 + Inertia.js*
