boolean('all_tenants', false); $query = BarobillSubscription::with(['member.tenant']) ->orderBy('member_id') ->orderBy('service_type'); // 테넌트 필터링 if (!$isHeadquarters && !$allTenants) { $query->whereHas('member', function ($q) use ($tenantId) { $q->where('tenant_id', $tenantId); }); } $subscriptions = $query->get(); if ($request->header('HX-Request')) { return response( view('barobill.billing.partials.subscription-table', [ 'subscriptions' => $subscriptions, 'allTenants' => $isHeadquarters || $allTenants, ])->render(), 200, ['Content-Type' => 'text/html'] ); } return response()->json([ 'success' => true, 'data' => $subscriptions, ]); } /** * 구독 등록/수정 */ public function saveSubscription(Request $request): JsonResponse { $validated = $request->validate([ 'member_id' => 'required|exists:barobill_members,id', 'service_type' => 'required|in:bank_account,card,hometax', 'monthly_fee' => 'nullable|integer|min:0', 'started_at' => 'nullable|date', 'ended_at' => 'nullable|date|after_or_equal:started_at', 'is_active' => 'nullable|boolean', 'memo' => 'nullable|string|max:500', ]); $subscription = $this->billingService->saveSubscription( $validated['member_id'], $validated['service_type'], $validated ); return response()->json([ 'success' => true, 'message' => '구독이 저장되었습니다.', 'data' => $subscription, ]); } /** * 구독 해지 */ public function cancelSubscription(int $id): JsonResponse { $result = $this->billingService->cancelSubscription($id); if (!$result) { return response()->json([ 'success' => false, 'message' => '구독을 찾을 수 없습니다.', ], 404); } return response()->json([ 'success' => true, 'message' => '구독이 해지되었습니다.', ]); } /** * 회원사별 구독 현황 조회 */ public function memberSubscriptions(int $memberId): JsonResponse|Response { $member = BarobillMember::with('tenant')->find($memberId); if (!$member) { return response()->json([ 'success' => false, 'message' => '회원사를 찾을 수 없습니다.', ], 404); } $subscriptions = BarobillSubscription::where('member_id', $memberId) ->orderBy('service_type') ->get(); return response()->json([ 'success' => true, 'data' => [ 'member' => $member, 'subscriptions' => $subscriptions, ], ]); } // ======================================== // 과금 현황 // ======================================== /** * 월별 과금 현황 목록 */ public function billingList(Request $request): JsonResponse|Response { $tenantId = session('selected_tenant_id'); $isHeadquarters = $tenantId == self::HEADQUARTERS_TENANT_ID; $allTenants = $request->boolean('all_tenants', false); $billingMonth = $request->input('billing_month', now()->format('Y-m')); // 테넌트 필터링 $filterTenantId = (!$isHeadquarters && !$allTenants) ? $tenantId : null; $summaries = BarobillMonthlySummary::with(['member.tenant']) ->where('billing_month', $billingMonth) ->when($filterTenantId, function ($q) use ($filterTenantId) { $q->whereHas('member', function ($q2) use ($filterTenantId) { $q2->where('tenant_id', $filterTenantId); }); }) ->orderBy('grand_total', 'desc') ->get(); $total = $this->billingService->getMonthlyTotal($billingMonth, $filterTenantId); if ($request->header('HX-Request')) { return response( view('barobill.billing.partials.billing-table', [ 'summaries' => $summaries, 'total' => $total, 'billingMonth' => $billingMonth, 'allTenants' => $isHeadquarters || $allTenants, ])->render(), 200, ['Content-Type' => 'text/html'] ); } return response()->json([ 'success' => true, 'data' => $summaries, 'total' => $total, ]); } /** * 월별 과금 통계 */ public function billingStats(Request $request): JsonResponse|Response { $tenantId = session('selected_tenant_id'); $isHeadquarters = $tenantId == self::HEADQUARTERS_TENANT_ID; $allTenants = $request->boolean('all_tenants', false); $billingMonth = $request->input('billing_month', now()->format('Y-m')); $filterTenantId = (!$isHeadquarters && !$allTenants) ? $tenantId : null; $stats = $this->billingService->getMonthlyTotal($billingMonth, $filterTenantId); if ($request->header('HX-Request')) { return response( view('barobill.billing.partials.billing-stats', compact('stats'))->render(), 200, ['Content-Type' => 'text/html'] ); } return response()->json([ 'success' => true, 'data' => $stats, ]); } /** * 회원사별 과금 상세 */ public function memberBilling(Request $request, int $memberId): JsonResponse|Response { $member = BarobillMember::with('tenant')->find($memberId); if (!$member) { return response()->json([ 'success' => false, 'message' => '회원사를 찾을 수 없습니다.', ], 404); } $billingMonth = $request->input('billing_month', now()->format('Y-m')); $records = BarobillBillingRecord::where('member_id', $memberId) ->where('billing_month', $billingMonth) ->orderBy('service_type') ->get(); $summary = BarobillMonthlySummary::where('member_id', $memberId) ->where('billing_month', $billingMonth) ->first(); if ($request->header('HX-Request')) { return response( view('barobill.billing.partials.member-billing-detail', [ 'member' => $member, 'records' => $records, 'summary' => $summary, 'billingMonth' => $billingMonth, ])->render(), 200, ['Content-Type' => 'text/html'] ); } return response()->json([ 'success' => true, 'data' => [ 'member' => $member, 'records' => $records, 'summary' => $summary, ], ]); } /** * 월별 과금 처리 (수동 실행) */ public function processBilling(Request $request): JsonResponse { $billingMonth = $request->input('billing_month', now()->format('Y-m')); $result = $this->billingService->processMonthlyBilling($billingMonth); return response()->json([ 'success' => true, 'message' => "과금 처리 완료: {$result['processed']}건 처리, {$result['skipped']}건 스킵", 'data' => $result, ]); } /** * 연간 추이 조회 */ public function yearlyTrend(Request $request): JsonResponse { $tenantId = session('selected_tenant_id'); $isHeadquarters = $tenantId == self::HEADQUARTERS_TENANT_ID; $allTenants = $request->boolean('all_tenants', false); $year = $request->input('year', now()->year); $filterTenantId = (!$isHeadquarters && !$allTenants) ? $tenantId : null; $trend = $this->billingService->getYearlyTrend($year, $filterTenantId); return response()->json([ 'success' => true, 'data' => $trend, ]); } /** * 엑셀 다운로드 */ public function export(Request $request) { $tenantId = session('selected_tenant_id'); $isHeadquarters = $tenantId == self::HEADQUARTERS_TENANT_ID; $allTenants = $request->boolean('all_tenants', false); $billingMonth = $request->input('billing_month', now()->format('Y-m')); $filterTenantId = (!$isHeadquarters && !$allTenants) ? $tenantId : null; $summaries = BarobillMonthlySummary::with(['member.tenant']) ->where('billing_month', $billingMonth) ->when($filterTenantId, function ($q) use ($filterTenantId) { $q->whereHas('member', function ($q2) use ($filterTenantId) { $q2->where('tenant_id', $filterTenantId); }); }) ->orderBy('grand_total', 'desc') ->get(); $total = $this->billingService->getMonthlyTotal($billingMonth, $filterTenantId); $filename = "barobill_billing_{$billingMonth}.csv"; $headers = [ 'Content-Type' => 'text/csv; charset=UTF-8', 'Content-Disposition' => "attachment; filename=\"{$filename}\"", ]; $callback = function () use ($summaries, $total, $isHeadquarters, $allTenants) { $file = fopen('php://output', 'w'); fprintf($file, chr(0xEF) . chr(0xBB) . chr(0xBF)); // 헤더 $headerRow = ['사업자번호', '상호', '계좌조회', '카드내역', '홈텍스', '월정액합계', '세금계산서(건)', '세금계산서(원)', '건별합계', '총합계']; if ($isHeadquarters || $allTenants) { array_unshift($headerRow, 'T-ID', '테넌트'); } fputcsv($file, $headerRow); // 데이터 foreach ($summaries as $summary) { $row = [ $summary->member->formatted_biz_no ?? '', $summary->member->corp_name ?? '', $summary->bank_account_fee, $summary->card_fee, $summary->hometax_fee, $summary->subscription_total, $summary->tax_invoice_count, $summary->tax_invoice_amount, $summary->usage_total, $summary->grand_total, ]; if ($isHeadquarters || $allTenants) { array_unshift($row, $summary->member->tenant_id ?? '', $summary->member->tenant->company_name ?? ''); } fputcsv($file, $row); } // 합계 $totalRow = ['', '합계', $total['bank_account_fee'], $total['card_fee'], $total['hometax_fee'], $total['subscription_total'], $total['tax_invoice_count'], $total['tax_invoice_amount'], $total['usage_total'], $total['grand_total']]; if ($isHeadquarters || $allTenants) { array_unshift($totalRow, '', ''); } fputcsv($file, $totalRow); fclose($file); }; return response()->stream($callback, 200, $headers); } // ======================================== // 과금 정책 관리 // ======================================== /** * 과금 정책 목록 조회 */ public function pricingPolicies(Request $request): JsonResponse|Response { $policies = BarobillPricingPolicy::orderBy('sort_order')->get(); if ($request->header('HX-Request')) { return response( view('barobill.billing.partials.pricing-policies-table', [ 'policies' => $policies, ])->render(), 200, ['Content-Type' => 'text/html'] ); } return response()->json([ 'success' => true, 'data' => $policies, ]); } /** * 과금 정책 수정 */ public function updatePricingPolicy(Request $request, int $id): JsonResponse { $policy = BarobillPricingPolicy::find($id); if (!$policy) { return response()->json([ 'success' => false, 'message' => '정책을 찾을 수 없습니다.', ], 404); } $validated = $request->validate([ 'name' => 'nullable|string|max:100', 'description' => 'nullable|string|max:255', 'free_quota' => 'nullable|integer|min:0', 'free_quota_unit' => 'nullable|string|max:20', 'additional_unit' => 'nullable|integer|min:1', 'additional_unit_label' => 'nullable|string|max:20', 'additional_price' => 'nullable|integer|min:0', 'is_active' => 'nullable|boolean', ]); $policy->update($validated); return response()->json([ 'success' => true, 'message' => '정책이 수정되었습니다.', 'data' => $policy->fresh(), ]); } /** * 과금 정책 단일 조회 */ public function getPricingPolicy(int $id): JsonResponse { $policy = BarobillPricingPolicy::find($id); if (!$policy) { return response()->json([ 'success' => false, 'message' => '정책을 찾을 수 없습니다.', ], 404); } return response()->json([ 'success' => true, 'data' => $policy, ]); } }