- earner 고정 행 → 동적 행 추가/삭제 구조로 변경 - 상호/성명 datalist 콤보박스 (드롭다운 선택 + 직접 입력) - display_name/business_reg_number 컬럼 직접 저장 - bulkSave: payment_id 기반 upsert + 미제출 draft 자동 삭제 - confirmed/paid 행 수정/삭제 불가 유지 - 엑셀 내보내기 display_name 직접 사용으로 단순화
219 lines
7.8 KiB
PHP
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();
|
|
}
|
|
}
|