2026-01-27 15:03:44 +09:00
|
|
|
<?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;
|
2026-01-27 15:17:25 +09:00
|
|
|
use App\Models\Barobill\BarobillPricingPolicy;
|
2026-01-27 15:03:44 +09:00
|
|
|
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');
|
|
|
|
|
|
|
|
|
|
// 테넌트 필터링
|
2026-02-25 11:45:01 +09:00
|
|
|
if (! $isHeadquarters && ! $allTenants) {
|
2026-01-27 15:03:44 +09:00
|
|
|
$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);
|
|
|
|
|
|
2026-02-25 11:45:01 +09:00
|
|
|
if (! $result) {
|
2026-01-27 15:03:44 +09:00
|
|
|
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);
|
2026-02-25 11:45:01 +09:00
|
|
|
if (! $member) {
|
2026-01-27 15:03:44 +09:00
|
|
|
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'));
|
|
|
|
|
|
|
|
|
|
// 테넌트 필터링
|
2026-02-25 11:45:01 +09:00
|
|
|
$filterTenantId = (! $isHeadquarters && ! $allTenants) ? $tenantId : null;
|
2026-01-27 15:03:44 +09:00
|
|
|
|
|
|
|
|
$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'));
|
|
|
|
|
|
2026-02-25 11:45:01 +09:00
|
|
|
$filterTenantId = (! $isHeadquarters && ! $allTenants) ? $tenantId : null;
|
2026-01-27 15:03:44 +09:00
|
|
|
$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);
|
2026-02-25 11:45:01 +09:00
|
|
|
if (! $member) {
|
2026-01-27 15:03:44 +09:00
|
|
|
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);
|
|
|
|
|
|
2026-02-25 11:45:01 +09:00
|
|
|
$filterTenantId = (! $isHeadquarters && ! $allTenants) ? $tenantId : null;
|
2026-01-27 15:03:44 +09:00
|
|
|
$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'));
|
|
|
|
|
|
2026-02-25 11:45:01 +09:00
|
|
|
$filterTenantId = (! $isHeadquarters && ! $allTenants) ? $tenantId : null;
|
2026-01-27 15:03:44 +09:00
|
|
|
|
|
|
|
|
$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');
|
2026-02-25 11:45:01 +09:00
|
|
|
fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF));
|
2026-01-27 15:03:44 +09:00
|
|
|
|
|
|
|
|
// 헤더
|
|
|
|
|
$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);
|
|
|
|
|
}
|
2026-01-27 15:17:25 +09:00
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// 과금 정책 관리
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 과금 정책 목록 조회
|
|
|
|
|
*/
|
|
|
|
|
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);
|
2026-02-25 11:45:01 +09:00
|
|
|
if (! $policy) {
|
2026-01-27 15:17:25 +09:00
|
|
|
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);
|
2026-02-25 11:45:01 +09:00
|
|
|
if (! $policy) {
|
2026-01-27 15:17:25 +09:00
|
|
|
return response()->json([
|
|
|
|
|
'success' => false,
|
|
|
|
|
'message' => '정책을 찾을 수 없습니다.',
|
|
|
|
|
], 404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'success' => true,
|
|
|
|
|
'data' => $policy,
|
|
|
|
|
]);
|
|
|
|
|
}
|
2026-01-27 15:03:44 +09:00
|
|
|
}
|