'integer', 'bonus_amount' => 'integer', 'deduction_amount' => 'integer', 'paid_at' => 'datetime', 'status' => PayrollStatus::class, 'total_amount' => 'integer', ]; } // 3. Scope (grouped by column, then alphabetical) // Column Group: status #[Scope] protected function paid(Builder $query): void { $query->where('status', PayrollStatus::PAID); } #[Scope] protected function unpaid(Builder $query): void { $query->where('status', PayrollStatus::UNPAID); } // 4. Attribute public function baseSalaryFormatted(): Attribute { return Attribute::make( get: fn () => $this->base_salary !== null ? 'Rp '.number_format($this->base_salary, 0, ',', '.') : '-', ); } public function bonusAmountFormatted(): Attribute { return Attribute::make( get: fn () => 'Rp '.number_format($this->bonus_amount, 0, ',', '.'), ); } public function canAdjust(): Attribute { return Attribute::make( get: fn () => $this->status === PayrollStatus::UNPAID && $this->relationLoaded('payrollPeriod') && $this->payrollPeriod?->isOpen(), ); } public function deductionAmountFormatted(): Attribute { return Attribute::make( get: fn () => 'Rp '.number_format($this->deduction_amount, 0, ',', '.'), ); } public function employeeName(): Attribute { return Attribute::make( get: fn () => $this->employee?->user?->profile?->full_name ?? $this->employee?->user?->username, ); } public function paidAtFormatted(): Attribute { return Attribute::make( get: fn () => $this->paid_at?->translatedFormat('l, d F Y H:i'), ); } public function statusLabel(): Attribute { return Attribute::make( get: fn () => $this->status?->label(), ); } public function totalAmountFormatted(): Attribute { return Attribute::make( get: fn () => 'Rp '.number_format($this->total_amount, 0, ',', '.'), ); } // 5. Other Methods public function calculateAdvanceDeduction(): int { $outstanding = (int) EmployeeAdvance::query() ->where('employee_id', $this->employee_id) ->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID]) ->selectRaw('SUM(amount - paid_amount) as remaining') ->value('remaining'); return min($outstanding, (int) $this->base_salary + (int) $this->adjustments() ->where('type', PayrollAdjustmentType::BONUS) ->sum('amount')); } public function recalculateAmounts(): void { $bonusAmount = (int) $this->adjustments() ->where('type', PayrollAdjustmentType::BONUS) ->sum('amount'); $manualDeduction = (int) $this->adjustments() ->where('type', PayrollAdjustmentType::DEDUCTION) ->sum('amount'); $advanceDeduction = $this->calculateAdvanceDeduction(); $this->bonus_amount = $bonusAmount; $this->deduction_amount = $advanceDeduction + $manualDeduction; $this->total_amount = max(0, (int) $this->base_salary + $bonusAmount - $this->deduction_amount); } // 6. Relation public function adjustments(): HasMany { return $this->hasMany(PayrollAdjustment::class); } public function cashTransaction(): BelongsTo { return $this->belongsTo(CashTransaction::class); } public function employee(): BelongsTo { return $this->belongsTo(Employee::class); } public function paidBy(): BelongsTo { return $this->belongsTo(User::class, 'paid_by_id'); } public function payrollPeriod(): BelongsTo { return $this->belongsTo(PayrollPeriod::class); } }