Files
sam-manage/app/Services/HR/BusinessIncomePaymentService.php
김보곤 30973d1772 feat: [hr] 사업소득자 임금대장 입력 기능 구현
- BusinessIncomePayment 모델 (소득세3%/지방소득세0.3% 자동계산)
- BusinessIncomePaymentService (일괄저장/통계/CSV내보내기)
- 웹/API 컨트롤러 (ALLOWED_PAYROLL_USERS 접근 제한)
- 스프레드시트 UI (인라인 편집, 실시간 세금 계산)
- HTMX 연월 변경 갱신, CSV 내보내기
2026-02-27 20:22:28 +09:00

180 lines
6.0 KiB
PHP

<?php
namespace App\Services\HR;
use App\Models\HR\BusinessIncomeEarner;
use App\Models\HR\BusinessIncomePayment;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
class BusinessIncomePaymentService
{
/**
* 월별 사업소득 지급 내역 조회
*/
public function getPayments(int $year, int $month): Collection
{
$tenantId = session('selected_tenant_id', 1);
return BusinessIncomePayment::query()
->with('user:id,name')
->forTenant($tenantId)
->forPeriod($year, $month)
->orderBy('id')
->get();
}
/**
* 활성 사업소득자 목록
*/
public function getActiveEarners(): Collection
{
$tenantId = session('selected_tenant_id', 1);
return BusinessIncomeEarner::query()
->with('user:id,name')
->forTenant($tenantId)
->activeEmployees()
->orderBy('display_name')
->get();
}
/**
* 일괄 저장
*
* - 지급총액 > 0: upsert (신규 생성 또는 draft 수정)
* - 지급총액 == 0: draft면 삭제, confirmed/paid는 무시
* - confirmed/paid 상태 레코드는 수정하지 않음
*/
public function bulkSave(int $year, int $month, array $items): array
{
$tenantId = session('selected_tenant_id', 1);
$saved = 0;
$deleted = 0;
$skipped = 0;
DB::transaction(function () use ($items, $tenantId, $year, $month, &$saved, &$deleted, &$skipped) {
foreach ($items as $item) {
$userId = (int) ($item['user_id'] ?? 0);
$grossAmount = (float) ($item['gross_amount'] ?? 0);
if ($userId === 0) {
continue;
}
// 기존 레코드 조회 (SoftDeletes 포함, 행 잠금)
$existing = BusinessIncomePayment::withTrashed()
->where('tenant_id', $tenantId)
->where('user_id', $userId)
->where('pay_year', $year)
->where('pay_month', $month)
->lockForUpdate()
->first();
if ($grossAmount <= 0) {
// 지급총액 0: draft면 삭제
if ($existing && ! $existing->trashed() && $existing->isEditable()) {
$existing->update(['deleted_by' => auth()->id()]);
$existing->delete();
$deleted++;
}
continue;
}
// confirmed/paid 상태는 수정하지 않음
if ($existing && ! $existing->trashed() && ! $existing->isEditable()) {
$skipped++;
continue;
}
$tax = BusinessIncomePayment::calculateTax($grossAmount);
$data = [
'tenant_id' => $tenantId,
'user_id' => $userId,
'pay_year' => $year,
'pay_month' => $month,
'service_content' => $item['service_content'] ?? null,
'gross_amount' => (int) $grossAmount,
'income_tax' => $tax['income_tax'],
'local_income_tax' => $tax['local_income_tax'],
'total_deductions' => $tax['total_deductions'],
'net_amount' => $tax['net_amount'],
'payment_date' => ! empty($item['payment_date']) ? $item['payment_date'] : null,
'note' => $item['note'] ?? null,
'updated_by' => auth()->id(),
];
if ($existing && $existing->trashed()) {
$existing->forceDelete();
}
if ($existing && ! $existing->trashed()) {
$existing->update($data);
} else {
$data['status'] = 'draft';
$data['created_by'] = auth()->id();
BusinessIncomePayment::create($data);
}
$saved++;
}
});
return [
'saved' => $saved,
'deleted' => $deleted,
'skipped' => $skipped,
];
}
/**
* 월간 통계 (통계 카드용)
*/
public function getMonthlyStats(int $year, int $month): array
{
$tenantId = session('selected_tenant_id', 1);
$result = BusinessIncomePayment::query()
->forTenant($tenantId)
->forPeriod($year, $month)
->select(
DB::raw('COUNT(*) as total_count'),
DB::raw('SUM(gross_amount) as total_gross'),
DB::raw('SUM(total_deductions) as total_deductions'),
DB::raw('SUM(net_amount) 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),
];
}
/**
* CSV 내보내기 데이터
*/
public function getExportData(int $year, int $month): Collection
{
$tenantId = session('selected_tenant_id', 1);
return BusinessIncomePayment::query()
->with('user:id,name')
->forTenant($tenantId)
->forPeriod($year, $month)
->orderBy('id')
->get();
}
}