Files
sam-manage/app/Http/Controllers/Finance/SettlementController.php
김보곤 c5edefc5a3 fix:구독관리 탭 조회조건 변경 (contracted→hq_status=handover)
인계(handover) 완료된 업체 = 구독 업체로 간주.
기존 contracted() 스코프 + subscription_fee 조건 대신
hq_status='handover' 조건으로 변경. 불필요한 상태 필터 제거.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 19:12:45 +09:00

519 lines
19 KiB
PHP

<?php
namespace App\Http\Controllers\Finance;
use App\Http\Controllers\Controller;
use App\Models\Sales\SalesCommission;
use App\Models\Sales\SalesContractProduct;
use App\Models\Sales\SalesPartner;
use App\Models\Sales\SalesTenantManagement;
use App\Services\SalesCommissionService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
class SettlementController extends Controller
{
public function __construct(
private SalesCommissionService $service
) {}
/**
* 통합 정산관리 메인 페이지
*/
public function index(Request $request): View|Response
{
if ($request->header('HX-Request') && !$request->header('HX-Boosted')) {
return response('', 200)->header('HX-Redirect', route('finance.settlement'));
}
$initialTab = $request->input('tab', 'commission');
// 수당 정산 탭 데이터 (기본 탭이므로 즉시 로드)
$year = $request->input('year', now()->year);
$month = $request->input('month', now()->month);
$filters = [
'scheduled_year' => $year,
'scheduled_month' => $month,
'status' => $request->input('status'),
'payment_type' => $request->input('payment_type'),
'partner_id' => $request->input('partner_id'),
'commission_type' => $request->input('commission_type'),
'search' => $request->input('search'),
];
$commissions = $this->service->getCommissions($filters);
$stats = $this->service->getSettlementStats($year, $month);
$partners = SalesPartner::with('user')
->active()
->orderBy('partner_code')
->get();
$pendingTenants = $this->service->getPendingPaymentTenants();
// 통합 통계 (페이지 상단)
$summaryStats = $this->getSummaryStats();
return view('finance.settlement.index', compact(
'initialTab',
'commissions',
'stats',
'partners',
'pendingTenants',
'year',
'month',
'filters',
'summaryStats'
));
}
/**
* 수당 통계카드 HTMX 갱신
*/
public function commissionStats(Request $request): View
{
$year = $request->input('year', now()->year);
$month = $request->input('month', now()->month);
$stats = $this->service->getSettlementStats($year, $month);
return view('finance.settlement.partials.commission.stats-cards', compact('stats', 'year', 'month'));
}
/**
* 수당 테이블 HTMX 갱신
*/
public function commissionTable(Request $request): View
{
$year = $request->input('year', now()->year);
$month = $request->input('month', now()->month);
$filters = [
'scheduled_year' => $year,
'scheduled_month' => $month,
'status' => $request->input('status'),
'payment_type' => $request->input('payment_type'),
'partner_id' => $request->input('partner_id'),
'commission_type' => $request->input('commission_type'),
'search' => $request->input('search'),
];
$commissions = $this->service->getCommissions($filters);
return view('finance.settlement.partials.commission.table', compact('commissions'));
}
/**
* 파트너별 현황 탭
*/
public function partnerSummary(Request $request): View
{
$query = SalesPartner::with('user');
// 검색
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search) {
$q->where('partner_code', 'like', "%{$search}%")
->orWhereHas('user', function ($uq) use ($search) {
$uq->where('name', 'like', "%{$search}%");
});
});
}
// 유형 필터
if ($type = $request->input('type')) {
if ($type === 'individual') {
$query->where('partner_type', '!=', 'corporate');
} elseif ($type === 'corporate') {
$query->where('partner_type', 'corporate');
}
}
// 상태 필터
if ($request->input('status', 'active') === 'active') {
$query->active();
}
$partners = $query->orderBy('partner_code')->paginate(20);
// 각 파트너별 수당 집계
$partnerIds = $partners->pluck('id')->toArray();
if (!empty($partnerIds)) {
$commissionStats = SalesCommission::selectRaw('
partner_id,
SUM(CASE WHEN status = "paid" THEN partner_commission ELSE 0 END) as paid_total,
SUM(CASE WHEN status IN ("pending", "approved") THEN partner_commission ELSE 0 END) as unpaid_total,
COUNT(*) as total_count,
MAX(CASE WHEN status = "paid" THEN actual_payment_date ELSE NULL END) as last_paid_date
')
->whereIn('partner_id', $partnerIds)
->groupBy('partner_id')
->get()
->keyBy('partner_id');
} else {
$commissionStats = collect();
}
return view('finance.settlement.partials.partner-summary', compact('partners', 'commissionStats'));
}
/**
* 컨설팅비용 탭
*/
public function consultingTab(Request $request): View
{
return view('finance.settlement.partials.consulting-tab');
}
/**
* 고객사정산 탭
*/
public function customerTab(Request $request): View
{
$query = SalesTenantManagement::with([
'tenant',
'tenantProspect.registeredBy.salesPartner',
'salesPartner.user',
'manager',
'commissions',
'contractProducts',
])->where('hq_status', '!=', SalesTenantManagement::HQ_STATUS_PENDING);
// 필터: 검색 (회사명)
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search) {
$q->whereHas('tenant', fn ($tq) => $tq->where('company_name', 'like', "%{$search}%"))
->orWhereHas('tenantProspect', fn ($pq) => $pq->where('company_name', 'like', "%{$search}%"));
});
}
// 필터: 개발 상태
if ($hqStatus = $request->input('hq_status')) {
$query->where('hq_status', $hqStatus);
}
// 필터: 담당 파트너 (salesPartner 또는 tenantProspect.registeredBy.salesPartner)
if ($partnerId = $request->input('partner_id')) {
$query->where(function ($q) use ($partnerId) {
$q->where('sales_partner_id', $partnerId)
->orWhereHas('tenantProspect.registeredBy.salesPartner', function ($sq) use ($partnerId) {
$sq->where('id', $partnerId);
});
});
}
// 필터: 수금 상태
$paymentStatus = $request->input('payment_status');
$managements = $query->orderByDesc('id')->paginate(20)->withQueryString();
// 수금 상태 필터 (컬렉션 레벨)
if ($paymentStatus) {
$managements->setCollection(
$managements->getCollection()->filter(function ($mgmt) use ($paymentStatus) {
$depositPaid = $mgmt->deposit_status === 'paid';
$balancePaid = $mgmt->balance_status === 'paid';
return match ($paymentStatus) {
'fully_paid' => $depositPaid && $balancePaid,
'partial' => ($depositPaid || $balancePaid) && !($depositPaid && $balancePaid),
'unpaid' => !$depositPaid && !$balancePaid,
default => true,
};
})
);
}
// 구독료 일괄 조회 (N+1 방지)
$tenantIds = $managements->getCollection()
->pluck('tenant_id')
->filter()
->unique()
->values()
->toArray();
$subscriptionFees = [];
if (!empty($tenantIds)) {
$subscriptionFees = SalesContractProduct::whereIn('tenant_id', $tenantIds)
->selectRaw('tenant_id, SUM(subscription_fee) as total_subscription_fee')
->groupBy('tenant_id')
->pluck('total_subscription_fee', 'tenant_id')
->toArray();
}
// 파트너 목록 (필터용)
$partners = SalesPartner::with('user')
->active()
->orderBy('partner_code')
->get();
// hqStatusLabels에서 pending 제외
$hqStatusLabels = collect(SalesTenantManagement::$hqStatusLabels)
->except(SalesTenantManagement::HQ_STATUS_PENDING)
->toArray();
// 통계 카드
$customerStats = $this->getCustomerStats();
return view('finance.settlement.partials.customer-tab', compact(
'managements',
'subscriptionFees',
'partners',
'hqStatusLabels',
'customerStats',
));
}
/**
* 구독관리 탭
*/
public function subscriptionTab(Request $request): View
{
// 인계(handover) 완료된 업체 = 구독 업체
$query = SalesTenantManagement::with([
'tenant', 'tenantProspect', 'salesPartner.user',
'manager', 'contractProducts.product', 'contractProducts.category',
])
->where('hq_status', 'handover');
// 검색 필터
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search) {
$q->whereHas('tenant', fn($t) => $t->where('company_name', 'like', "%{$search}%"))
->orWhereHas('tenantProspect', fn($t) => $t->where('company_name', 'like', "%{$search}%"));
});
}
$managements = $query->orderBy('contracted_at', 'desc')->get();
// 통계 계산
$stats = [
'activeCount' => $managements->count(),
'monthlyRecurring' => $managements->sum(fn($m) => $m->contractProducts->sum('subscription_fee')),
'totalProducts' => $managements->sum(fn($m) => $m->contractProducts->where('subscription_fee', '>', 0)->count()),
];
$stats['yearlyRecurring'] = $stats['monthlyRecurring'] * 12;
return view('finance.settlement.partials.subscription-tab', compact('managements', 'stats'));
}
/**
* 수당 지급 탭 (파트너별 그룹핑)
*/
public function paymentTab(Request $request): View
{
// approved 상태 수당을 partner_id 기준 GROUP BY
$partnerPayments = SalesCommission::where('status', SalesCommission::STATUS_APPROVED)
->selectRaw('
partner_id,
GROUP_CONCAT(id) as commission_ids,
COUNT(*) as count,
SUM(partner_commission) as partner_total,
SUM(manager_commission) as manager_total,
SUM(COALESCE(referrer_commission, 0)) as referrer_total
')
->groupBy('partner_id')
->get();
// 파트너 정보 eager load
$partners = SalesPartner::with('user')
->whereIn('id', $partnerPayments->pluck('partner_id'))
->get()
->keyBy('id');
// 통계 카드 데이터
$now = now();
$paymentStats = [
'waiting_count' => $partnerPayments->sum('count'),
'waiting_amount' => $partnerPayments->sum(fn ($p) => $p->partner_total + $p->manager_total + $p->referrer_total),
'this_month_paid_count' => SalesCommission::where('status', SalesCommission::STATUS_PAID)
->whereYear('actual_payment_date', $now->year)
->whereMonth('actual_payment_date', $now->month)
->count(),
'this_month_paid_amount' => SalesCommission::where('status', SalesCommission::STATUS_PAID)
->whereYear('actual_payment_date', $now->year)
->whereMonth('actual_payment_date', $now->month)
->selectRaw('SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total')
->value('total') ?? 0,
'partner_total' => $partnerPayments->sum('partner_total'),
'manager_referrer_total' => $partnerPayments->sum('manager_total') + $partnerPayments->sum('referrer_total'),
];
return view('finance.settlement.partials.payment-tab', compact('partnerPayments', 'partners', 'paymentStats'));
}
/**
* 파트너별 수당 건 상세 (HTMX partial)
*/
public function paymentPartnerDetail(int $partnerId): View
{
$commissions = SalesCommission::where('status', SalesCommission::STATUS_APPROVED)
->where('partner_id', $partnerId)
->with(['management.tenant', 'manager'])
->orderBy('scheduled_payment_date')
->get();
return view('finance.settlement.partials.payment-partner-detail', compact('commissions', 'partnerId'));
}
/**
* 수당지급현황통계 페이지
*/
public function paymentStats(Request $request): View|Response
{
if ($request->header('HX-Request') && !$request->header('HX-Boosted')) {
return response('', 200)->header('HX-Redirect', route('finance.settlement.payment-stats'));
}
$year = (int) $request->input('year', now()->year);
// 통계 카드
$totalPaidAmount = SalesCommission::where('status', SalesCommission::STATUS_PAID)
->whereYear('actual_payment_date', $year)
->selectRaw('SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total')
->value('total') ?? 0;
$totalPaidCount = SalesCommission::where('status', SalesCommission::STATUS_PAID)
->whereYear('actual_payment_date', $year)
->count();
$activePartners = SalesCommission::where('status', SalesCommission::STATUS_PAID)
->whereYear('actual_payment_date', $year)
->distinct('partner_id')
->count('partner_id');
$avgCommission = $totalPaidCount > 0 ? round($totalPaidAmount / $totalPaidCount) : 0;
$statsCards = [
'total_paid_amount' => $totalPaidAmount,
'total_paid_count' => $totalPaidCount,
'active_partners' => $activePartners,
'avg_commission' => $avgCommission,
];
// 차트 1 & 4: 월별 지급 추이 (해당 연도)
$monthlyTrend = SalesCommission::where('status', SalesCommission::STATUS_PAID)
->whereYear('actual_payment_date', $year)
->selectRaw("
DATE_FORMAT(actual_payment_date, '%Y-%m') as month,
SUM(partner_commission) as partner_total,
SUM(manager_commission) as manager_total,
SUM(COALESCE(referrer_commission, 0)) as referrer_total,
COUNT(*) as count
")
->groupByRaw("DATE_FORMAT(actual_payment_date, '%Y-%m')")
->orderBy('month')
->get();
// 차트 2: 수당 유형별 비율
$typeRatio = SalesCommission::where('status', SalesCommission::STATUS_PAID)
->whereYear('actual_payment_date', $year)
->selectRaw("
SUM(partner_commission) as partner_total,
SUM(manager_commission) as manager_total,
SUM(COALESCE(referrer_commission, 0)) as referrer_total
")
->first();
// 차트 3: 파트너별 수당 Top 10
$topPartners = SalesCommission::where('status', SalesCommission::STATUS_PAID)
->whereYear('actual_payment_date', $year)
->selectRaw('partner_id, SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total')
->groupBy('partner_id')
->orderByDesc('total')
->limit(10)
->get();
$topPartnerNames = SalesPartner::with('user')
->whereIn('id', $topPartners->pluck('partner_id'))
->get()
->keyBy('id');
return view('finance.settlement.payment-stats', compact(
'year', 'statsCards', 'monthlyTrend', 'typeRatio', 'topPartners', 'topPartnerNames'
));
}
/**
* 고객사정산 통계 데이터
*/
private function getCustomerStats(): array
{
$baseQuery = SalesTenantManagement::where('hq_status', '!=', SalesTenantManagement::HQ_STATUS_PENDING);
// 총 개발비
$totalFee = (clone $baseQuery)->sum('total_registration_fee');
$totalCount = (clone $baseQuery)->count();
// 수금완료 (deposit + balance 모두 paid인 건의 합계)
$collectedAmount = (clone $baseQuery)
->where('deposit_status', 'paid')
->sum('deposit_amount')
+ (clone $baseQuery)
->where('balance_status', 'paid')
->sum('balance_amount');
// 미수금
$uncollectedAmount = $totalFee - $collectedAmount;
// 개발 진행 중 (handover 제외)
$inProgressCount = (clone $baseQuery)
->where('hq_status', '!=', SalesTenantManagement::HQ_STATUS_HANDOVER)
->count();
// 구독 전환 (handover + tenant active)
$subscriptionCount = SalesTenantManagement::where('hq_status', SalesTenantManagement::HQ_STATUS_HANDOVER)
->whereHas('tenant', fn ($q) => $q->whereNull('deleted_at'))
->count();
return [
'total_fee' => $totalFee,
'total_count' => $totalCount,
'collected_amount' => $collectedAmount,
'uncollected_amount' => max(0, $uncollectedAmount),
'in_progress_count' => $inProgressCount,
'subscription_count' => $subscriptionCount,
];
}
/**
* 통합 통계 데이터
*/
private function getSummaryStats(): array
{
$now = now();
// 미지급 수당 (pending + approved)
$unpaidAmount = SalesCommission::whereIn('status', [
SalesCommission::STATUS_PENDING,
SalesCommission::STATUS_APPROVED,
])->selectRaw('SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total')
->value('total') ?? 0;
// 승인 대기 건수
$pendingCount = SalesCommission::where('status', SalesCommission::STATUS_PENDING)->count();
// 이번달 지급예정
$thisMonthScheduled = SalesCommission::whereIn('status', [
SalesCommission::STATUS_PENDING,
SalesCommission::STATUS_APPROVED,
])
->whereYear('scheduled_payment_date', $now->year)
->whereMonth('scheduled_payment_date', $now->month)
->selectRaw('SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total')
->value('total') ?? 0;
// 누적 지급완료
$totalPaid = SalesCommission::where('status', SalesCommission::STATUS_PAID)
->selectRaw('SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total')
->value('total') ?? 0;
return [
'unpaid_amount' => $unpaidAmount,
'pending_count' => $pendingCount,
'this_month_scheduled' => $thisMonthScheduled,
'total_paid' => $totalPaid,
];
}
}