Files
sam-manage/app/Http/Controllers/Api/Admin/Barobill/BarobillBillingController.php
2026-02-25 11:45:01 +09:00

460 lines
15 KiB
PHP

<?php
namespace App\Http\Controllers\Api\Admin\Barobill;
use App\Http\Controllers\Controller;
use App\Models\Barobill\BarobillBillingRecord;
use App\Models\Barobill\BarobillMember;
use App\Models\Barobill\BarobillMonthlySummary;
use App\Models\Barobill\BarobillPricingPolicy;
use App\Models\Barobill\BarobillSubscription;
use App\Services\Barobill\BarobillBillingService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
/**
* 바로빌 과금 관리 API 컨트롤러
*/
class BarobillBillingController extends Controller
{
private const HEADQUARTERS_TENANT_ID = 1;
public function __construct(
protected BarobillBillingService $billingService
) {}
// ========================================
// 구독 관리
// ========================================
/**
* 구독 목록 조회
*/
public function subscriptions(Request $request): JsonResponse|Response
{
$tenantId = session('selected_tenant_id');
$isHeadquarters = $tenantId == self::HEADQUARTERS_TENANT_ID;
$allTenants = $request->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,
]);
}
}