Files
sam-manage/app/Services/HR/BusinessIncomePaymentService.php
김보곤 2ac4c188d5 feat: [hr] 사업소득자 임금대장 동적 행 입력 리디자인
- earner 고정 행 → 동적 행 추가/삭제 구조로 변경
- 상호/성명 datalist 콤보박스 (드롭다운 선택 + 직접 입력)
- display_name/business_reg_number 컬럼 직접 저장
- bulkSave: payment_id 기반 upsert + 미제출 draft 자동 삭제
- confirmed/paid 행 수정/삭제 불가 유지
- 엑셀 내보내기 display_name 직접 사용으로 단순화
2026-03-03 14:21:06 +09:00

219 lines
7.8 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();
}
/**
* 일괄 저장
*
* - payment_id 기반 기존 레코드 조회 (수정 시)
* - payment_id 없고 user_id 있으면 기존 방식 조회
* - 둘 다 없으면 신규 생성
* - 지급총액 == 0: draft면 삭제, confirmed/paid는 무시
* - 제출되지 않은 기존 draft 행 자동 삭제 (사용자가 행을 삭제한 경우)
*/
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) {
$submittedPaymentIds = [];
foreach ($items as $item) {
$paymentId = ! empty($item['payment_id']) ? (int) $item['payment_id'] : null;
$userId = ! empty($item['user_id']) ? (int) $item['user_id'] : null;
$displayName = trim($item['display_name'] ?? '');
$businessRegNumber = $item['business_reg_number'] ?? null;
$grossAmount = (float) ($item['gross_amount'] ?? 0);
if (empty($displayName)) {
continue;
}
$existing = null;
// payment_id로 기존 레코드 조회
if ($paymentId) {
$existing = BusinessIncomePayment::where('id', $paymentId)
->where('tenant_id', $tenantId)
->lockForUpdate()
->first();
}
// user_id로 기존 레코드 조회 (payment_id 없고 user_id 있는 경우)
if (! $existing && $userId) {
$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()) {
$submittedPaymentIds[] = $existing->id;
$skipped++;
continue;
}
$tax = BusinessIncomePayment::calculateTax($grossAmount);
$data = [
'tenant_id' => $tenantId,
'user_id' => $userId,
'display_name' => $displayName,
'business_reg_number' => $businessRegNumber,
'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();
$existing = null;
}
if ($existing) {
$existing->update($data);
$submittedPaymentIds[] = $existing->id;
} else {
$data['status'] = 'draft';
$data['created_by'] = auth()->id();
$record = BusinessIncomePayment::create($data);
$submittedPaymentIds[] = $record->id;
}
$saved++;
}
// 제출되지 않은 기존 draft 행 자동 삭제 (사용자가 행을 삭제한 경우)
$orphanDrafts = BusinessIncomePayment::where('tenant_id', $tenantId)
->where('pay_year', $year)
->where('pay_month', $month)
->where('status', 'draft')
->when(count($submittedPaymentIds) > 0, fn ($q) => $q->whereNotIn('id', $submittedPaymentIds))
->get();
foreach ($orphanDrafts as $orphan) {
$orphan->update(['deleted_by' => auth()->id()]);
$orphan->delete();
$deleted++;
}
});
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),
];
}
/**
* XLSX 내보내기 데이터
*/
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();
}
}