diff --git a/app/Http/Controllers/Api/Admin/Barobill/BarobillBillingController.php b/app/Http/Controllers/Api/Admin/Barobill/BarobillBillingController.php new file mode 100644 index 00000000..ab6822a9 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/Barobill/BarobillBillingController.php @@ -0,0 +1,379 @@ +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); + } +} diff --git a/app/Http/Controllers/Barobill/BarobillController.php b/app/Http/Controllers/Barobill/BarobillController.php index ae04b9f4..acae8706 100644 --- a/app/Http/Controllers/Barobill/BarobillController.php +++ b/app/Http/Controllers/Barobill/BarobillController.php @@ -140,4 +140,26 @@ public function usage(Request $request): View|Response return view('barobill.usage.index', compact('currentTenant', 'barobillMember', 'isHeadquarters')); } + + /** + * 과금관리 페이지 (본사 전용) + */ + public function billing(Request $request): View|Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('barobill.billing.index')); + } + + // 현재 선택된 테넌트 정보 + $tenantId = session('selected_tenant_id', 1); + $currentTenant = Tenant::find($tenantId); + + // 본사(테넌트 1) 여부 + $isHeadquarters = $tenantId == 1; + + // 테넌트 목록 (전체 테넌트 모드에서 선택용) + $tenants = Tenant::select('id', 'company_name')->orderBy('company_name')->get(); + + return view('barobill.billing.index', compact('currentTenant', 'isHeadquarters', 'tenants')); + } } diff --git a/app/Models/Barobill/BarobillBillingRecord.php b/app/Models/Barobill/BarobillBillingRecord.php new file mode 100644 index 00000000..2584ebe8 --- /dev/null +++ b/app/Models/Barobill/BarobillBillingRecord.php @@ -0,0 +1,116 @@ + 'integer', + 'unit_price' => 'integer', + 'total_amount' => 'integer', + 'billed_at' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 서비스 유형 라벨 + */ + public const SERVICE_TYPES = [ + 'tax_invoice' => '전자세금계산서', + 'bank_account' => '계좌조회', + 'card' => '카드내역', + 'hometax' => '홈텍스 매입/매출', + ]; + + /** + * 과금 유형 라벨 + */ + public const BILLING_TYPES = [ + 'subscription' => '월정액', + 'usage' => '건별', + ]; + + /** + * 건별 단가 (원) + */ + public const USAGE_UNIT_PRICES = [ + 'tax_invoice' => 100, // 전자세금계산서 건당 100원 + ]; + + /** + * 바로빌 회원사 관계 + */ + public function member(): BelongsTo + { + return $this->belongsTo(BarobillMember::class, 'member_id'); + } + + /** + * 서비스 유형 라벨 + */ + public function getServiceTypeLabelAttribute(): string + { + return self::SERVICE_TYPES[$this->service_type] ?? $this->service_type; + } + + /** + * 과금 유형 라벨 + */ + public function getBillingTypeLabelAttribute(): string + { + return self::BILLING_TYPES[$this->billing_type] ?? $this->billing_type; + } + + /** + * 특정 월 조회 + */ + public function scopeOfMonth($query, string $billingMonth) + { + return $query->where('billing_month', $billingMonth); + } + + /** + * 월정액만 조회 + */ + public function scopeSubscription($query) + { + return $query->where('billing_type', 'subscription'); + } + + /** + * 건별만 조회 + */ + public function scopeUsage($query) + { + return $query->where('billing_type', 'usage'); + } + + /** + * 특정 서비스 조회 + */ + public function scopeOfService($query, string $serviceType) + { + return $query->where('service_type', $serviceType); + } +} diff --git a/app/Models/Barobill/BarobillMonthlySummary.php b/app/Models/Barobill/BarobillMonthlySummary.php new file mode 100644 index 00000000..f5cdabe7 --- /dev/null +++ b/app/Models/Barobill/BarobillMonthlySummary.php @@ -0,0 +1,141 @@ + 'integer', + 'card_fee' => 'integer', + 'hometax_fee' => 'integer', + 'subscription_total' => 'integer', + 'tax_invoice_count' => 'integer', + 'tax_invoice_amount' => 'integer', + 'usage_total' => 'integer', + 'grand_total' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 바로빌 회원사 관계 + */ + public function member(): BelongsTo + { + return $this->belongsTo(BarobillMember::class, 'member_id'); + } + + /** + * 특정 월 조회 + */ + public function scopeOfMonth($query, string $billingMonth) + { + return $query->where('billing_month', $billingMonth); + } + + /** + * 집계 데이터 갱신 또는 생성 + */ + public static function updateOrCreateSummary(int $memberId, string $billingMonth): self + { + // 해당 월의 과금 기록 조회 + $records = BarobillBillingRecord::where('member_id', $memberId) + ->where('billing_month', $billingMonth) + ->get(); + + $data = [ + 'bank_account_fee' => 0, + 'card_fee' => 0, + 'hometax_fee' => 0, + 'subscription_total' => 0, + 'tax_invoice_count' => 0, + 'tax_invoice_amount' => 0, + 'usage_total' => 0, + 'grand_total' => 0, + ]; + + foreach ($records as $record) { + if ($record->billing_type === 'subscription') { + switch ($record->service_type) { + case 'bank_account': + $data['bank_account_fee'] = $record->total_amount; + break; + case 'card': + $data['card_fee'] = $record->total_amount; + break; + case 'hometax': + $data['hometax_fee'] = $record->total_amount; + break; + } + $data['subscription_total'] += $record->total_amount; + } else { + // 건별 사용 + if ($record->service_type === 'tax_invoice') { + $data['tax_invoice_count'] = $record->quantity; + $data['tax_invoice_amount'] = $record->total_amount; + } + $data['usage_total'] += $record->total_amount; + } + } + + $data['grand_total'] = $data['subscription_total'] + $data['usage_total']; + + return self::updateOrCreate( + ['member_id' => $memberId, 'billing_month' => $billingMonth], + $data + ); + } + + /** + * 전체 회원사 월별 합계 + */ + public static function getMonthlyTotal(string $billingMonth): array + { + $result = self::where('billing_month', $billingMonth) + ->selectRaw(' + COUNT(*) as member_count, + SUM(bank_account_fee) as bank_account_fee, + SUM(card_fee) as card_fee, + SUM(hometax_fee) as hometax_fee, + SUM(subscription_total) as subscription_total, + SUM(tax_invoice_count) as tax_invoice_count, + SUM(tax_invoice_amount) as tax_invoice_amount, + SUM(usage_total) as usage_total, + SUM(grand_total) as grand_total + ') + ->first(); + + return [ + 'member_count' => (int) ($result->member_count ?? 0), + 'bank_account_fee' => (int) ($result->bank_account_fee ?? 0), + 'card_fee' => (int) ($result->card_fee ?? 0), + 'hometax_fee' => (int) ($result->hometax_fee ?? 0), + 'subscription_total' => (int) ($result->subscription_total ?? 0), + 'tax_invoice_count' => (int) ($result->tax_invoice_count ?? 0), + 'tax_invoice_amount' => (int) ($result->tax_invoice_amount ?? 0), + 'usage_total' => (int) ($result->usage_total ?? 0), + 'grand_total' => (int) ($result->grand_total ?? 0), + ]; + } +} diff --git a/app/Models/Barobill/BarobillSubscription.php b/app/Models/Barobill/BarobillSubscription.php new file mode 100644 index 00000000..4bfcae47 --- /dev/null +++ b/app/Models/Barobill/BarobillSubscription.php @@ -0,0 +1,119 @@ + 'integer', + 'started_at' => 'date', + 'ended_at' => 'date', + 'is_active' => 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * 서비스 유형 라벨 + */ + public const SERVICE_TYPES = [ + 'bank_account' => '계좌조회', + 'card' => '카드내역', + 'hometax' => '홈텍스 매입/매출', + ]; + + /** + * 기본 월정액 (원) + */ + public const DEFAULT_MONTHLY_FEES = [ + 'bank_account' => 10000, + 'card' => 10000, + 'hometax' => 10000, + ]; + + /** + * 바로빌 회원사 관계 + */ + public function member(): BelongsTo + { + return $this->belongsTo(BarobillMember::class, 'member_id'); + } + + /** + * 서비스 유형 라벨 + */ + public function getServiceTypeLabelAttribute(): string + { + return self::SERVICE_TYPES[$this->service_type] ?? $this->service_type; + } + + /** + * 상태 라벨 + */ + public function getStatusLabelAttribute(): string + { + if (!$this->is_active) { + return '비활성'; + } + if ($this->ended_at && $this->ended_at->isPast()) { + return '종료'; + } + return '구독중'; + } + + /** + * 상태 색상 클래스 + */ + public function getStatusColorAttribute(): string + { + if (!$this->is_active) { + return 'bg-gray-100 text-gray-800'; + } + if ($this->ended_at && $this->ended_at->isPast()) { + return 'bg-red-100 text-red-800'; + } + return 'bg-green-100 text-green-800'; + } + + /** + * 활성 구독만 조회 + */ + public function scopeActive($query) + { + return $query->where('is_active', true) + ->where(function ($q) { + $q->whereNull('ended_at') + ->orWhere('ended_at', '>=', now()->toDateString()); + }); + } + + /** + * 특정 서비스 유형 조회 + */ + public function scopeOfService($query, string $serviceType) + { + return $query->where('service_type', $serviceType); + } +} diff --git a/app/Services/Barobill/BarobillBillingService.php b/app/Services/Barobill/BarobillBillingService.php new file mode 100644 index 00000000..f139503b --- /dev/null +++ b/app/Services/Barobill/BarobillBillingService.php @@ -0,0 +1,252 @@ +orderBy('service_type') + ->get() + ->toArray(); + } + + /** + * 구독 등록/수정 + */ + public function saveSubscription(int $memberId, string $serviceType, array $data): BarobillSubscription + { + return BarobillSubscription::updateOrCreate( + [ + 'member_id' => $memberId, + 'service_type' => $serviceType, + ], + [ + 'monthly_fee' => $data['monthly_fee'] ?? BarobillSubscription::DEFAULT_MONTHLY_FEES[$serviceType] ?? 0, + 'started_at' => $data['started_at'] ?? now()->toDateString(), + 'ended_at' => $data['ended_at'] ?? null, + 'is_active' => $data['is_active'] ?? true, + 'memo' => $data['memo'] ?? null, + ] + ); + } + + /** + * 구독 해지 + */ + public function cancelSubscription(int $subscriptionId): bool + { + $subscription = BarobillSubscription::find($subscriptionId); + if (!$subscription) { + return false; + } + + $subscription->update([ + 'ended_at' => now()->toDateString(), + 'is_active' => false, + ]); + + return true; + } + + /** + * 월별 과금 처리 (배치용) + * + * 매월 1일에 실행하여 전월 구독료 과금 + */ + public function processMonthlyBilling(?string $billingMonth = null): array + { + $billingMonth = $billingMonth ?? now()->format('Y-m'); + $billedAt = Carbon::parse($billingMonth . '-01'); + + $results = [ + 'billing_month' => $billingMonth, + 'processed' => 0, + 'skipped' => 0, + 'errors' => [], + ]; + + // 활성 구독 조회 + $subscriptions = BarobillSubscription::active() + ->with('member') + ->get(); + + foreach ($subscriptions as $subscription) { + try { + // 이미 과금된 기록이 있는지 확인 + $exists = BarobillBillingRecord::where('member_id', $subscription->member_id) + ->where('billing_month', $billingMonth) + ->where('service_type', $subscription->service_type) + ->where('billing_type', 'subscription') + ->exists(); + + if ($exists) { + $results['skipped']++; + continue; + } + + // 과금 기록 생성 + BarobillBillingRecord::create([ + 'member_id' => $subscription->member_id, + 'billing_month' => $billingMonth, + 'service_type' => $subscription->service_type, + 'billing_type' => 'subscription', + 'quantity' => 1, + 'unit_price' => $subscription->monthly_fee, + 'total_amount' => $subscription->monthly_fee, + 'billed_at' => $billedAt, + 'description' => BarobillSubscription::SERVICE_TYPES[$subscription->service_type] . ' 월정액', + ]); + + $results['processed']++; + + } catch (\Exception $e) { + $results['errors'][] = [ + 'member_id' => $subscription->member_id, + 'service_type' => $subscription->service_type, + 'error' => $e->getMessage(), + ]; + Log::error('월정액 과금 처리 실패', [ + 'subscription_id' => $subscription->id, + 'error' => $e->getMessage(), + ]); + } + } + + // 월별 집계 갱신 + $this->updateMonthlySummaries($billingMonth); + + return $results; + } + + /** + * 건별 사용량 과금 (세금계산서) + */ + public function recordUsage(int $memberId, string $serviceType, int $quantity, ?string $billingMonth = null): BarobillBillingRecord + { + $billingMonth = $billingMonth ?? now()->format('Y-m'); + $unitPrice = BarobillBillingRecord::USAGE_UNIT_PRICES[$serviceType] ?? 0; + + $record = BarobillBillingRecord::updateOrCreate( + [ + 'member_id' => $memberId, + 'billing_month' => $billingMonth, + 'service_type' => $serviceType, + 'billing_type' => 'usage', + ], + [ + 'quantity' => $quantity, + 'unit_price' => $unitPrice, + 'total_amount' => $quantity * $unitPrice, + 'billed_at' => now()->toDateString(), + 'description' => BarobillBillingRecord::SERVICE_TYPES[$serviceType] . ' ' . $quantity . '건', + ] + ); + + // 집계 갱신 + BarobillMonthlySummary::updateOrCreateSummary($memberId, $billingMonth); + + return $record; + } + + /** + * 월별 집계 갱신 + */ + public function updateMonthlySummaries(string $billingMonth): void + { + // 해당 월에 과금 기록이 있는 회원사 조회 + $memberIds = BarobillBillingRecord::where('billing_month', $billingMonth) + ->distinct() + ->pluck('member_id'); + + foreach ($memberIds as $memberId) { + BarobillMonthlySummary::updateOrCreateSummary($memberId, $billingMonth); + } + } + + /** + * 월별 과금 현황 조회 + */ + public function getMonthlyBillingList(string $billingMonth, ?int $tenantId = null): array + { + $query = BarobillMonthlySummary::with(['member.tenant']) + ->where('billing_month', $billingMonth); + + if ($tenantId) { + $query->whereHas('member', function ($q) use ($tenantId) { + $q->where('tenant_id', $tenantId); + }); + } + + return $query->orderBy('grand_total', 'desc')->get()->toArray(); + } + + /** + * 월별 합계 조회 + */ + public function getMonthlyTotal(string $billingMonth, ?int $tenantId = null): array + { + $query = BarobillMonthlySummary::where('billing_month', $billingMonth); + + if ($tenantId) { + $query->whereHas('member', function ($q) use ($tenantId) { + $q->where('tenant_id', $tenantId); + }); + } + + $result = $query->selectRaw(' + COUNT(*) as member_count, + SUM(bank_account_fee) as bank_account_fee, + SUM(card_fee) as card_fee, + SUM(hometax_fee) as hometax_fee, + SUM(subscription_total) as subscription_total, + SUM(tax_invoice_count) as tax_invoice_count, + SUM(tax_invoice_amount) as tax_invoice_amount, + SUM(usage_total) as usage_total, + SUM(grand_total) as grand_total + ')->first(); + + return [ + 'billing_month' => $billingMonth, + 'member_count' => (int) ($result->member_count ?? 0), + 'bank_account_fee' => (int) ($result->bank_account_fee ?? 0), + 'card_fee' => (int) ($result->card_fee ?? 0), + 'hometax_fee' => (int) ($result->hometax_fee ?? 0), + 'subscription_total' => (int) ($result->subscription_total ?? 0), + 'tax_invoice_count' => (int) ($result->tax_invoice_count ?? 0), + 'tax_invoice_amount' => (int) ($result->tax_invoice_amount ?? 0), + 'usage_total' => (int) ($result->usage_total ?? 0), + 'grand_total' => (int) ($result->grand_total ?? 0), + ]; + } + + /** + * 연간 과금 추이 조회 + */ + public function getYearlyTrend(int $year, ?int $tenantId = null): array + { + $months = []; + for ($m = 1; $m <= 12; $m++) { + $billingMonth = sprintf('%d-%02d', $year, $m); + $months[$billingMonth] = $this->getMonthlyTotal($billingMonth, $tenantId); + } + return $months; + } +} diff --git a/resources/views/barobill/billing/index.blade.php b/resources/views/barobill/billing/index.blade.php new file mode 100644 index 00000000..30637a00 --- /dev/null +++ b/resources/views/barobill/billing/index.blade.php @@ -0,0 +1,354 @@ +@extends('layouts.app') + +@section('title', '과금관리') + +@section('content') + +@if($currentTenant) +
조회 기준월
+{{ now()->format('Y년 m월') }}
+과금 유형
+월정액 + 건별
+결제일
+매월 1일
+바로빌 월정액 구독 및 과금 현황 관리
+{{ $billingMonth }} 기준 과금 내역이 없습니다.
+"과금 처리" 버튼을 클릭하여 월정액을 처리하세요.
+| + T-ID + | ++ 테넌트 + | + @endif ++ 사업자번호 + | ++ 상호명 + | ++ 계좌조회 + | ++ 카드내역 + | ++ 홈텍스 + | ++ 월정액 + | ++ 세금계산서 + | ++ 총합계 + | ++ 상세 + | +
|---|---|---|---|---|---|---|---|---|---|---|
| + + {{ $summary->member->tenant_id ?? '-' }} + + | ++ + {{ $summary->member->tenant->company_name ?? '-' }} + + | + @endif +
+ {{ $summary->member->formatted_biz_no ?? '-' }}
+ |
+
+ {{ $summary->member->corp_name ?? '-' }}
+ |
+ + + {{ number_format($summary->bank_account_fee) }} + + | ++ + {{ number_format($summary->card_fee) }} + + | ++ + | ++ + {{ number_format($summary->subscription_total) }} + + | +
+ @if($summary->tax_invoice_count > 0)
+
+ {{ number_format($summary->tax_invoice_count) }}건
+ ({{ number_format($summary->tax_invoice_amount) }})
+
+ @else
+ -
+ @endif
+ |
+ + + {{ number_format($summary->grand_total) }} + + | ++ + | +
| + @endif + | + 합계 + | ++ {{ number_format($total['bank_account_fee']) }} + | ++ {{ number_format($total['card_fee']) }} + | ++ {{ number_format($total['hometax_fee']) }} + | ++ {{ number_format($total['subscription_total']) }} + | ++ {{ number_format($total['tax_invoice_amount']) }} + | ++ {{ number_format($total['grand_total']) }}원 + | ++ | ||
{{ $member->corp_name }}
+{{ $member->formatted_biz_no }}
+{{ $member->tenant->company_name ?? '-' }}
+{{ $billingMonth }}
+{{ $record->service_type_label }}
++ {{ $record->billing_type_label }} + @if($record->billing_type === 'usage') + ({{ number_format($record->quantity) }}건 x {{ number_format($record->unit_price) }}원) + @endif +
+{{ number_format($record->total_amount) }}원
+{{ $record->billed_at?->format('Y-m-d') }}
+이 기간에 과금 내역이 없습니다.
+월정액 합계
+{{ number_format($summary->subscription_total) }}원
+건별 합계
+{{ number_format($summary->usage_total) }}원
+회원사에 월정액 서비스 구독을 등록해주세요.
+| + T-ID + | ++ 테넌트 + | + @endif ++ 회원사 + | ++ 서비스 + | ++ 월정액 + | ++ 시작일 + | ++ 종료일 + | ++ 상태 + | ++ 관리 + | +
|---|---|---|---|---|---|---|---|---|
| + + {{ $subscription->member->tenant_id ?? '-' }} + + | ++ + {{ $subscription->member->tenant->company_name ?? '-' }} + + | + @endif +
+ {{ $subscription->member->corp_name ?? '-' }}
+ {{ $subscription->member->formatted_biz_no ?? '' }}
+ |
+ + @php + $serviceColors = [ + 'bank_account' => 'bg-green-100 text-green-800', + 'card' => 'bg-blue-100 text-blue-800', + 'hometax' => 'bg-orange-100 text-orange-800', + ]; + @endphp + + {{ $subscription->service_type_label }} + + | ++ {{ number_format($subscription->monthly_fee) }}원 + | ++ {{ $subscription->started_at?->format('Y-m-d') ?? '-' }} + | ++ {{ $subscription->ended_at?->format('Y-m-d') ?? '-' }} + | ++ + {{ $subscription->status_label }} + + | +
+
+ @if($subscription->is_active && !$subscription->ended_at)
+
+ @endif
+
+ |
+