Files
sam-manage/app/Services/HR/PayrollService.php
김보곤 bcb45c9362 feat: [payroll] 급여 확정 취소 기능 추가
- 확정 상태에서 작성중으로 되돌리는 기능 추가
- Model: isUnconfirmable() 상태 헬퍼 추가
- Service: unconfirmPayroll() 메서드 추가
- Controller: unconfirm() 엔드포인트 추가
- Route: POST /{id}/unconfirm 라우트 추가
- View: 확정 취소 버튼 및 JS 함수 추가
2026-02-27 22:17:34 +09:00

781 lines
29 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services\HR;
use App\Models\HR\Employee;
use App\Models\HR\IncomeTaxBracket;
use App\Models\HR\Payroll;
use App\Models\HR\PayrollSetting;
use App\Models\Tenants\Department;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
class PayrollService
{
private const TAX_TABLE_YEAR = 2024;
/**
* 필터 적용 쿼리 생성 (목록/엑셀 공통)
*/
private function buildFilteredQuery(array $filters = [])
{
$tenantId = session('selected_tenant_id', 1);
$query = Payroll::query()
->with(['user', 'user.tenantProfiles' => fn ($q) => $q->where('tenant_id', $tenantId), 'user.tenantProfiles.department'])
->forTenant($tenantId);
if (! empty($filters['q'])) {
$search = $filters['q'];
$query->whereHas('user', function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%");
});
}
if (! empty($filters['department_id'])) {
$deptId = $filters['department_id'];
$query->whereHas('user.tenantProfiles', function ($q) use ($tenantId, $deptId) {
$q->where('tenant_id', $tenantId)->where('department_id', $deptId);
});
}
if (! empty($filters['status'])) {
$query->where('status', $filters['status']);
}
$year = $filters['year'] ?? now()->year;
$month = $filters['month'] ?? now()->month;
$query->forPeriod((int) $year, (int) $month);
return $query->orderBy('created_at', 'desc');
}
/**
* 급여 목록 조회 (페이지네이션)
*/
public function getPayrolls(array $filters = [], int $perPage = 20): LengthAwarePaginator
{
return $this->buildFilteredQuery($filters)->paginate($perPage);
}
/**
* 엑셀 내보내기용 데이터 (전체)
*/
public function getExportData(array $filters = []): Collection
{
return $this->buildFilteredQuery($filters)->get();
}
/**
* 월간 통계 (통계 카드용)
*/
public function getMonthlyStats(?int $year = null, ?int $month = null): array
{
$tenantId = session('selected_tenant_id', 1);
$year = $year ?? now()->year;
$month = $month ?? now()->month;
$result = Payroll::query()
->forTenant($tenantId)
->forPeriod($year, $month)
->select(
DB::raw('COUNT(*) as total_count'),
DB::raw('SUM(gross_salary) as total_gross'),
DB::raw('SUM(total_deductions) as total_deductions'),
DB::raw('SUM(net_salary) as total_net'),
DB::raw("SUM(CASE WHEN status = 'draft' THEN 1 ELSE 0 END) as draft_count"),
DB::raw("SUM(CASE WHEN status = 'confirmed' THEN 1 ELSE 0 END) as confirmed_count"),
DB::raw("SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as paid_count"),
)
->first();
return [
'total_gross' => (int) ($result->total_gross ?? 0),
'total_deductions' => (int) ($result->total_deductions ?? 0),
'total_net' => (int) ($result->total_net ?? 0),
'total_count' => (int) ($result->total_count ?? 0),
'draft_count' => (int) ($result->draft_count ?? 0),
'confirmed_count' => (int) ($result->confirmed_count ?? 0),
'paid_count' => (int) ($result->paid_count ?? 0),
'year' => $year,
'month' => $month,
];
}
/**
* 급여 등록
*/
public function storePayroll(array $data): Payroll
{
$tenantId = session('selected_tenant_id', 1);
return DB::transaction(function () use ($data, $tenantId) {
// 동일 대상/기간 중복 체크 (트랜잭션 내 + 행 잠금으로 Race Condition 방지)
$existing = Payroll::withTrashed()
->where('tenant_id', $tenantId)
->where('user_id', $data['user_id'])
->where('pay_year', $data['pay_year'])
->where('pay_month', $data['pay_month'])
->lockForUpdate()
->first();
if ($existing) {
if ($existing->trashed()) {
$existing->forceDelete();
} else {
// 이미 존재하는 레코드가 있으면 수정 모드로 전환
return $this->updatePayroll($existing->id, $data);
}
}
$familyCount = $data['family_count'] ?? $this->resolveFamilyCount($data['user_id']);
$calculated = $this->calculateAmounts($data, null, $familyCount);
$this->applyDeductionOverrides($calculated, $data['deduction_overrides'] ?? null);
$payroll = Payroll::create([
'tenant_id' => $tenantId,
'user_id' => $data['user_id'],
'pay_year' => $data['pay_year'],
'pay_month' => $data['pay_month'],
'base_salary' => $data['base_salary'] ?? 0,
'overtime_pay' => $data['overtime_pay'] ?? 0,
'bonus' => $data['bonus'] ?? 0,
'allowances' => $data['allowances'] ?? null,
'gross_salary' => $calculated['gross_salary'],
'income_tax' => $calculated['income_tax'],
'resident_tax' => $calculated['resident_tax'],
'health_insurance' => $calculated['health_insurance'],
'long_term_care' => $calculated['long_term_care'],
'pension' => $calculated['pension'],
'employment_insurance' => $calculated['employment_insurance'],
'deductions' => $data['deductions'] ?? null,
'total_deductions' => $calculated['total_deductions'],
'net_salary' => $calculated['net_salary'],
'status' => 'draft',
'note' => $data['note'] ?? null,
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
return $payroll->load('user');
});
}
/**
* 급여 수정
*/
public function updatePayroll(int $id, array $data): ?Payroll
{
$tenantId = session('selected_tenant_id', 1);
$payroll = Payroll::query()
->forTenant($tenantId)
->find($id);
if (! $payroll || ! $payroll->isEditable()) {
return null;
}
// 지급 항목 (변경된 값 또는 기존값 유지)
$baseSalary = (float) ($data['base_salary'] ?? $payroll->base_salary);
$overtimePay = (float) ($data['overtime_pay'] ?? $payroll->overtime_pay);
$bonus = (float) ($data['bonus'] ?? $payroll->bonus);
$allowances = array_key_exists('allowances', $data) ? $data['allowances'] : $payroll->allowances;
$allowancesTotal = 0;
$allowancesArr = is_string($allowances) ? json_decode($allowances, true) : $allowances;
foreach ($allowancesArr ?? [] as $allowance) {
$allowancesTotal += (float) ($allowance['amount'] ?? 0);
}
$grossSalary = (int) ($baseSalary + $overtimePay + $bonus + $allowancesTotal);
// 공제 항목 (수동 수정값 우선, 없으면 기존 저장값 유지 — 요율 재계산 안 함)
$overrides = $data['deduction_overrides'] ?? [];
$incomeTax = isset($overrides['income_tax']) ? (int) $overrides['income_tax'] : $payroll->income_tax;
$residentTax = isset($overrides['resident_tax']) ? (int) $overrides['resident_tax'] : $payroll->resident_tax;
$healthInsurance = isset($overrides['health_insurance']) ? (int) $overrides['health_insurance'] : $payroll->health_insurance;
$longTermCare = isset($overrides['long_term_care']) ? (int) $overrides['long_term_care'] : $payroll->long_term_care;
$pension = isset($overrides['pension']) ? (int) $overrides['pension'] : $payroll->pension;
$employmentInsurance = isset($overrides['employment_insurance']) ? (int) $overrides['employment_insurance'] : $payroll->employment_insurance;
$deductions = array_key_exists('deductions', $data) ? $data['deductions'] : $payroll->deductions;
// 추가 공제 합계
$extraDeductions = 0;
$deductionsArr = is_string($deductions) ? json_decode($deductions, true) : $deductions;
foreach ($deductionsArr ?? [] as $deduction) {
$extraDeductions += (float) ($deduction['amount'] ?? 0);
}
$totalDeductions = (int) ($incomeTax + $residentTax + $healthInsurance + $longTermCare + $pension + $employmentInsurance + $extraDeductions);
$netSalary = (int) max(0, $grossSalary - $totalDeductions);
$payroll->update([
'base_salary' => $baseSalary,
'overtime_pay' => $overtimePay,
'bonus' => $bonus,
'allowances' => $allowances,
'gross_salary' => $grossSalary,
'income_tax' => $incomeTax,
'resident_tax' => $residentTax,
'health_insurance' => $healthInsurance,
'long_term_care' => $longTermCare,
'pension' => $pension,
'employment_insurance' => $employmentInsurance,
'deductions' => $deductions,
'total_deductions' => $totalDeductions,
'net_salary' => $netSalary,
'note' => $data['note'] ?? $payroll->note,
'updated_by' => auth()->id(),
]);
return $payroll->fresh(['user']);
}
/**
* 급여 삭제
*/
public function deletePayroll(int $id): bool
{
$tenantId = session('selected_tenant_id', 1);
$payroll = Payroll::query()
->forTenant($tenantId)
->find($id);
if (! $payroll || ! $payroll->isDeletable()) {
return false;
}
$payroll->update(['deleted_by' => auth()->id()]);
$payroll->delete();
return true;
}
/**
* 급여 확정
*/
public function confirmPayroll(int $id): ?Payroll
{
$tenantId = session('selected_tenant_id', 1);
$payroll = Payroll::query()
->forTenant($tenantId)
->find($id);
if (! $payroll || ! $payroll->isConfirmable()) {
return null;
}
$payroll->update([
'status' => Payroll::STATUS_CONFIRMED,
'confirmed_at' => now(),
'confirmed_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
return $payroll->fresh(['user']);
}
/**
* 급여 확정 취소 (confirmed → draft)
*/
public function unconfirmPayroll(int $id): ?Payroll
{
$tenantId = session('selected_tenant_id', 1);
$payroll = Payroll::query()
->forTenant($tenantId)
->find($id);
if (! $payroll || ! $payroll->isUnconfirmable()) {
return null;
}
$payroll->update([
'status' => Payroll::STATUS_DRAFT,
'confirmed_at' => null,
'confirmed_by' => null,
'updated_by' => auth()->id(),
]);
return $payroll->fresh(['user']);
}
/**
* 급여 지급 처리
*/
public function payPayroll(int $id): ?Payroll
{
$tenantId = session('selected_tenant_id', 1);
$payroll = Payroll::query()
->forTenant($tenantId)
->find($id);
if (! $payroll || ! $payroll->isPayable()) {
return null;
}
$payroll->update([
'status' => Payroll::STATUS_PAID,
'paid_at' => now(),
'updated_by' => auth()->id(),
]);
return $payroll->fresh(['user']);
}
/**
* 전월 급여 복사 등록
*/
public function copyFromPreviousMonth(int $year, int $month): array
{
$tenantId = session('selected_tenant_id', 1);
// 전월 계산 (1월 → 전년 12월)
$prevYear = $month === 1 ? $year - 1 : $year;
$prevMonth = $month === 1 ? 12 : $month - 1;
$previousPayrolls = Payroll::query()
->forTenant($tenantId)
->forPeriod($prevYear, $prevMonth)
->get();
if ($previousPayrolls->isEmpty()) {
return ['created' => 0, 'skipped' => 0, 'no_previous' => true];
}
$created = 0;
$skipped = 0;
DB::transaction(function () use ($previousPayrolls, $tenantId, $year, $month, &$created, &$skipped) {
foreach ($previousPayrolls as $prev) {
// SoftDeletes 포함하여 유니크 제약 충돌 방지
$existing = Payroll::withTrashed()
->where('tenant_id', $tenantId)
->where('user_id', $prev->user_id)
->forPeriod($year, $month)
->first();
if ($existing && ! $existing->trashed()) {
$skipped++;
continue;
}
// soft-deleted 레코드가 있으면 삭제 후 재생성
if ($existing && $existing->trashed()) {
$existing->forceDelete();
}
Payroll::create([
'tenant_id' => $tenantId,
'user_id' => $prev->user_id,
'pay_year' => $year,
'pay_month' => $month,
'base_salary' => $prev->base_salary,
'overtime_pay' => $prev->overtime_pay,
'bonus' => $prev->bonus,
'allowances' => $prev->allowances,
'gross_salary' => $prev->gross_salary,
'income_tax' => $prev->income_tax,
'resident_tax' => $prev->resident_tax,
'health_insurance' => $prev->health_insurance,
'long_term_care' => $prev->long_term_care,
'pension' => $prev->pension,
'employment_insurance' => $prev->employment_insurance,
'deductions' => $prev->deductions,
'total_deductions' => $prev->total_deductions,
'net_salary' => $prev->net_salary,
'status' => 'draft',
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
$created++;
}
});
return ['created' => $created, 'skipped' => $skipped];
}
/**
* 일괄 생성 (재직 사원 전체)
*/
public function bulkGenerate(int $year, int $month): array
{
$tenantId = session('selected_tenant_id', 1);
$settings = PayrollSetting::getOrCreate($tenantId);
$created = 0;
$skipped = 0;
$employees = Employee::query()
->with('user:id,name')
->forTenant($tenantId)
->activeEmployees()
->get();
DB::transaction(function () use ($employees, $tenantId, $year, $month, $settings, &$created, &$skipped) {
foreach ($employees as $employee) {
// SoftDeletes 포함하여 유니크 제약 충돌 방지
$existing = Payroll::withTrashed()
->where('tenant_id', $tenantId)
->where('user_id', $employee->user_id)
->forPeriod($year, $month)
->first();
if ($existing && ! $existing->trashed()) {
$skipped++;
continue;
}
// soft-deleted 레코드가 있으면 삭제 후 재생성
if ($existing && $existing->trashed()) {
$existing->forceDelete();
}
$annualSalary = $employee->getJsonExtraValue('salary', 0);
$baseSalary = $annualSalary > 0 ? round($annualSalary / 12) : 0;
$data = [
'base_salary' => $baseSalary,
'overtime_pay' => 0,
'bonus' => 0,
'allowances' => null,
'deductions' => null,
];
// 본인(1) + is_dependent=true인 피부양자 수
$familyCount = 1 + collect($employee->dependents)
->where('is_dependent', true)->count();
$calculated = $this->calculateAmounts($data, $settings, $familyCount);
Payroll::create([
'tenant_id' => $tenantId,
'user_id' => $employee->user_id,
'pay_year' => $year,
'pay_month' => $month,
'base_salary' => $baseSalary,
'overtime_pay' => 0,
'bonus' => 0,
'allowances' => null,
'gross_salary' => $calculated['gross_salary'],
'income_tax' => $calculated['income_tax'],
'resident_tax' => $calculated['resident_tax'],
'health_insurance' => $calculated['health_insurance'],
'long_term_care' => $calculated['long_term_care'],
'pension' => $calculated['pension'],
'employment_insurance' => $calculated['employment_insurance'],
'deductions' => null,
'total_deductions' => $calculated['total_deductions'],
'net_salary' => $calculated['net_salary'],
'status' => 'draft',
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
$created++;
}
});
return ['created' => $created, 'skipped' => $skipped];
}
/**
* 급여 금액 자동 계산
*
* 식대(bonus)는 비과세 항목으로, 총 지급액에는 포함되지만
* 4대보험 및 세금 산출 기준(과세표준)에서는 제외된다.
*/
public function calculateAmounts(array $data, ?PayrollSetting $settings = null, int $familyCount = 1): array
{
$settings = $settings ?? PayrollSetting::getOrCreate();
$baseSalary = (float) ($data['base_salary'] ?? 0);
$overtimePay = (float) ($data['overtime_pay'] ?? 0);
$bonus = (float) ($data['bonus'] ?? 0); // 식대 (비과세)
// 수당 합계
$allowancesTotal = 0;
if (! empty($data['allowances'])) {
$allowances = is_string($data['allowances']) ? json_decode($data['allowances'], true) : $data['allowances'];
foreach ($allowances ?? [] as $allowance) {
$allowancesTotal += (float) ($allowance['amount'] ?? 0);
}
}
// 총 지급액 (비과세 포함)
$grossSalary = $baseSalary + $overtimePay + $bonus + $allowancesTotal;
// 과세표준 = 총 지급액 - 식대(비과세)
$taxableBase = $grossSalary - $bonus;
// 4대보험 계산 (과세표준 기준)
$healthInsurance = $this->calculateHealthInsurance($taxableBase, $settings);
$longTermCare = $this->calculateLongTermCare($taxableBase, $settings);
$pension = $this->calculatePension($taxableBase, $settings);
$employmentInsurance = $this->calculateEmploymentInsurance($taxableBase, $settings);
// 근로소득세 (간이세액표, 과세표준 기준, 공제대상가족수 반영)
$incomeTax = $this->calculateIncomeTax($taxableBase, $familyCount);
// 지방소득세 (근로소득세의 10%, 10원 단위 절삭)
$residentTax = (int) (floor($incomeTax * ($settings->resident_tax_rate / 100) / 10) * 10);
// 추가 공제 합계
$extraDeductions = 0;
if (! empty($data['deductions'])) {
$deductions = is_string($data['deductions']) ? json_decode($data['deductions'], true) : $data['deductions'];
foreach ($deductions ?? [] as $deduction) {
$extraDeductions += (float) ($deduction['amount'] ?? 0);
}
}
// 총 공제액
$totalDeductions = $incomeTax + $residentTax + $healthInsurance + $longTermCare + $pension + $employmentInsurance + $extraDeductions;
// 실수령액
$netSalary = $grossSalary - $totalDeductions;
return [
'gross_salary' => (int) $grossSalary,
'taxable_base' => (int) $taxableBase,
'income_tax' => $incomeTax,
'resident_tax' => $residentTax,
'health_insurance' => $healthInsurance,
'long_term_care' => $longTermCare,
'pension' => $pension,
'employment_insurance' => $employmentInsurance,
'total_deductions' => (int) $totalDeductions,
'net_salary' => (int) max(0, $netSalary),
];
}
/**
* 수동 수정된 공제 항목 반영
*/
private function applyDeductionOverrides(array &$calculated, ?array $overrides): void
{
if (empty($overrides)) {
return;
}
// 추가공제 금액 산출 (오버라이드 적용 전 법정공제 합계와 비교)
$oldStatutory = $calculated['pension'] + $calculated['health_insurance'] + $calculated['long_term_care']
+ $calculated['employment_insurance'] + $calculated['income_tax'] + $calculated['resident_tax'];
$extraDeductions = max(0, $calculated['total_deductions'] - $oldStatutory);
// 수동 수정값 적용
$fields = ['pension', 'health_insurance', 'long_term_care', 'employment_insurance', 'income_tax', 'resident_tax'];
foreach ($fields as $field) {
if (isset($overrides[$field])) {
$calculated[$field] = (int) $overrides[$field];
}
}
// 총 공제액·실수령액 재계산
$newStatutory = $calculated['pension'] + $calculated['health_insurance'] + $calculated['long_term_care']
+ $calculated['employment_insurance'] + $calculated['income_tax'] + $calculated['resident_tax'];
$calculated['total_deductions'] = (int) ($newStatutory + $extraDeductions);
$calculated['net_salary'] = (int) max(0, $calculated['gross_salary'] - $calculated['total_deductions']);
}
/**
* 근로소득세 계산 (2024 국세청 간이세액표 기반)
*
* @param float $taxableBase 과세표준 (원)
* @param int $familyCount 공제대상가족수 (1~11, 본인 포함)
*/
public function calculateIncomeTax(float $taxableBase, int $familyCount = 1): int
{
if ($taxableBase <= 0) {
return 0;
}
$salaryThousand = (int) floor($taxableBase / 1000);
$familyCount = max(1, min(11, $familyCount));
// 770천원 미만: 세액 없음
if ($salaryThousand < 770) {
return 0;
}
// 10,000천원 초과: 공식 계산
if ($salaryThousand > 10000) {
return $this->calculateHighIncomeTax($salaryThousand, $familyCount);
}
// DB 조회 (간이세액표)
return IncomeTaxBracket::lookupTax(self::TAX_TABLE_YEAR, $salaryThousand, $familyCount);
}
/**
* 10,000천원 초과 구간 근로소득세 공식 계산
*
* 소득세법 시행령 별표2 기준:
* - 10,000~14,000천원: (1천만원 세액) + (초과분 × 98% × 35%) + 25,000
* - 14,000~28,000천원: (1천만원 세액) + 1,397,000 + (초과분 × 98% × 38%)
* - 28,000~30,000천원: (1천만원 세액) + 6,610,600 + (초과분 × 98% × 40%)
* - 30,000~45,000천원: (1천만원 세액) + 7,394,600 + (초과분 × 40%)
* - 45,000~87,000천원: (1천만원 세액) + 13,394,600 + (초과분 × 42%)
* - 87,000천원 초과: (1천만원 세액) + 31,034,600 + (초과분 × 45%)
*/
private function calculateHighIncomeTax(int $salaryThousand, int $familyCount): int
{
// 1천만원(10,000천원) 정확값의 가족수별 세액 (DB에서 조회)
$baseTax = IncomeTaxBracket::where('tax_year', self::TAX_TABLE_YEAR)
->where('salary_from', 10000)
->whereColumn('salary_from', 'salary_to')
->where('family_count', $familyCount)
->value('tax_amount') ?? 0;
// 초과분 계산 (천원 단위 → 원 단위)
$excessThousand = $salaryThousand - 10000;
$excessWon = $excessThousand * 1000;
if ($salaryThousand <= 14000) {
$tax = $baseTax + ($excessWon * 0.98 * 0.35) + 25000;
} elseif ($salaryThousand <= 28000) {
$excessWon = ($salaryThousand - 14000) * 1000;
$tax = $baseTax + 1397000 + ($excessWon * 0.98 * 0.38);
} elseif ($salaryThousand <= 30000) {
$excessWon = ($salaryThousand - 28000) * 1000;
$tax = $baseTax + 6610600 + ($excessWon * 0.98 * 0.40);
} elseif ($salaryThousand <= 45000) {
$excessWon = ($salaryThousand - 30000) * 1000;
$tax = $baseTax + 7394600 + ($excessWon * 0.40);
} elseif ($salaryThousand <= 87000) {
$excessWon = ($salaryThousand - 45000) * 1000;
$tax = $baseTax + 13394600 + ($excessWon * 0.42);
} else {
// 87,000천원 초과
$excessWon = ($salaryThousand - 87000) * 1000;
$tax = $baseTax + 31034600 + ($excessWon * 0.45);
}
// 10원 단위 절삭
return (int) (floor($tax / 10) * 10);
}
/**
* 건강보험료 계산
*/
private function calculateHealthInsurance(float $grossSalary, PayrollSetting $settings): int
{
return (int) (floor($grossSalary * ($settings->health_insurance_rate / 100) / 10) * 10);
}
/**
* 장기요양보험료 계산 (건강보험료의 일정 비율)
*/
private function calculateLongTermCare(float $grossSalary, PayrollSetting $settings): int
{
$healthInsurance = $grossSalary * ($settings->health_insurance_rate / 100);
return (int) (floor($healthInsurance * ($settings->long_term_care_rate / 100) / 10) * 10);
}
/**
* 국민연금 계산
*/
private function calculatePension(float $grossSalary, PayrollSetting $settings): int
{
$base = min(max($grossSalary, (float) $settings->pension_min_salary), (float) $settings->pension_max_salary);
return (int) (floor($base * ($settings->pension_rate / 100) / 10) * 10);
}
/**
* 고용보험료 계산
*/
private function calculateEmploymentInsurance(float $grossSalary, PayrollSetting $settings): int
{
return (int) (floor($grossSalary * ($settings->employment_insurance_rate / 100) / 10) * 10);
}
/**
* 부서 목록 (드롭다운용)
*/
public function getDepartments(): Collection
{
$tenantId = session('selected_tenant_id', 1);
return Department::query()
->where('is_active', true)
->when($tenantId, fn ($q) => $q->where('tenant_id', $tenantId))
->orderBy('sort_order')
->orderBy('name')
->get(['id', 'name', 'code']);
}
/**
* 활성 사원 목록 (드롭다운용)
*/
public function getActiveEmployees(): Collection
{
$tenantId = session('selected_tenant_id', 1);
return Employee::query()
->with('user:id,name')
->forTenant($tenantId)
->activeEmployees()
->orderBy('display_name')
->get(['id', 'user_id', 'display_name', 'department_id', 'json_extra']);
}
/**
* 급여 설정 조회/생성
*/
public function getSettings(): PayrollSetting
{
return PayrollSetting::getOrCreate();
}
/**
* user_id로 공제대상가족수 산출 (본인 1 + is_dependent 피부양자)
*/
public function resolveFamilyCount(int $userId): int
{
$tenantId = session('selected_tenant_id', 1);
$employee = Employee::query()
->forTenant($tenantId)
->where('user_id', $userId)
->first(['json_extra']);
if (! $employee) {
return 1;
}
$dependentCount = collect($employee->dependents)
->where('is_dependent', true)
->count();
return max(1, min(11, 1 + $dependentCount));
}
/**
* 급여 설정 수정
*/
public function updateSettings(array $data): PayrollSetting
{
$settings = PayrollSetting::getOrCreate();
$settings->update([
'health_insurance_rate' => $data['health_insurance_rate'] ?? $settings->health_insurance_rate,
'long_term_care_rate' => $data['long_term_care_rate'] ?? $settings->long_term_care_rate,
'pension_rate' => $data['pension_rate'] ?? $settings->pension_rate,
'employment_insurance_rate' => $data['employment_insurance_rate'] ?? $settings->employment_insurance_rate,
'pension_max_salary' => $data['pension_max_salary'] ?? $settings->pension_max_salary,
'pension_min_salary' => $data['pension_min_salary'] ?? $settings->pension_min_salary,
'pay_day' => $data['pay_day'] ?? $settings->pay_day,
'auto_calculate' => $data['auto_calculate'] ?? $settings->auto_calculate,
]);
return $settings->fresh();
}
}