- BusinessIncomePayment 모델 (소득세3%/지방소득세0.3% 자동계산) - BusinessIncomePaymentService (일괄저장/통계/CSV내보내기) - 웹/API 컨트롤러 (ALLOWED_PAYROLL_USERS 접근 제한) - 스프레드시트 UI (인라인 편집, 실시간 세금 계산) - HTMX 연월 변경 갱신, CSV 내보내기
180 lines
6.0 KiB
PHP
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();
|
|
}
|
|
}
|