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(); } }