with([ 'tenant', 'partner.user.parent', 'manager', 'referrerPartner.user', 'management.tenant', 'management.tenantProspect.registeredBy.parent', 'management.tenantProspect.registeredBy.salesPartner', 'management.salesPartner.user.parent', 'management.manager', 'management.contractProducts', ]); // 상태 필터 if (! empty($filters['status'])) { $query->where('status', $filters['status']); } // 입금구분 필터 if (! empty($filters['payment_type'])) { $query->where('payment_type', $filters['payment_type']); } // 영업파트너 필터 if (! empty($filters['partner_id'])) { $query->where('partner_id', $filters['partner_id']); } // 매니저 필터 if (! empty($filters['manager_user_id'])) { $query->where('manager_user_id', $filters['manager_user_id']); } // 지급예정 기간 범위 필터 if (! empty($filters['scheduled_start_year']) && ! empty($filters['scheduled_start_month']) && ! empty($filters['scheduled_end_year']) && ! empty($filters['scheduled_end_month'])) { $startDate = \Carbon\Carbon::create($filters['scheduled_start_year'], $filters['scheduled_start_month'], 1)->startOfMonth(); $endDate = \Carbon\Carbon::create($filters['scheduled_end_year'], $filters['scheduled_end_month'], 1)->endOfMonth(); $query->whereBetween('scheduled_payment_date', [$startDate, $endDate]); } // 지급예정 년/월 필터 (단일) elseif (! empty($filters['scheduled_year']) && ! empty($filters['scheduled_month'])) { $query->forScheduledMonth((int) $filters['scheduled_year'], (int) $filters['scheduled_month']); } // 입금일 기간 필터 if (! empty($filters['payment_start_date']) && ! empty($filters['payment_end_date'])) { $query->paymentDateBetween($filters['payment_start_date'], $filters['payment_end_date']); } // 수당유형 필터 if (! empty($filters['commission_type'])) { $commissionType = $filters['commission_type']; if ($commissionType === 'partner') { $query->where('partner_commission', '>', 0); } elseif ($commissionType === 'manager') { $query->where('manager_commission', '>', 0); } elseif ($commissionType === 'referrer') { $query->whereNotNull('referrer_partner_id') ->where('referrer_commission', '>', 0); } } // 고객사 검색 (management → tenant 또는 tenantProspect) if (! empty($filters['search'])) { $search = $filters['search']; $query->whereHas('management', function ($q) use ($search) { $q->where(function ($sub) use ($search) { $sub->whereHas('tenant', function ($tq) use ($search) { $tq->where('company_name', 'like', "%{$search}%"); })->orWhereHas('tenantProspect', function ($tpq) use ($search) { $tpq->where('company_name', 'like', "%{$search}%"); }); }); }); } return $query ->orderBy('scheduled_payment_date', 'desc') ->orderBy('created_at', 'desc') ->paginate($perPage); } /** * 정산 상세 조회 */ public function getCommissionById(int $id): ?SalesCommission { return SalesCommission::with([ 'tenant', 'partner.user', 'manager', 'management', 'details.contractProduct.product', 'approver', ])->find($id); } // ========================================================================= // 수당 생성 (입금 시) // ========================================================================= /** * 입금 등록 및 수당 생성 */ public function createCommission(int $managementId, string $paymentType, float $paymentAmount, string $paymentDate): SalesCommission { return DB::transaction(function () use ($managementId, $paymentType, $paymentAmount, $paymentDate) { $management = SalesTenantManagement::with([ 'salesPartner.user.parent', 'contractProducts.product', 'tenantProspect.registeredBy.salesPartner', ])->findOrFail($managementId); // 영업파트너 resolve (fallback: tenantProspect → registeredBy → salesPartner) $partner = $management->salesPartner; if (! $partner) { $partner = $management->tenantProspect?->registeredBy?->salesPartner; } if (! $partner) { throw new \Exception('영업파트너가 지정되지 않았습니다.'); } $paymentDateCarbon = Carbon::parse($paymentDate); // 계약 상품이 없으면 기본 계산 $contractProducts = $management->contractProducts; $totalRegistrationFee = $contractProducts->sum('registration_fee') ?: $paymentAmount * 2; $baseAmount = $totalRegistrationFee / 2; // 개발비의 50% // 수당률 (단체/개인 분기 처리) $isGroup = $partner->isGroup(); if ($isGroup) { // 단체: 단체 30%, 유치자 3%, 매니저 0% $partnerRate = $partner->commission_rate ?? self::DEFAULT_GROUP_RATE; $referrerId = $partner->referrer_partner_id; $referrerRate = $referrerId ? self::DEFAULT_GROUP_REFERRER_RATE : 0; } else { // 개인: 파트너 20%, 유치자(상위파트너) 5% $partnerRate = $partner->commission_rate ?? self::DEFAULT_PARTNER_RATE; // 협업지원금: 유치자(parent)의 SalesPartner에게 5% $parentUser = $partner->user?->parent; $referrerPartner = $parentUser ? SalesPartner::where('user_id', $parentUser->id)->first() : null; $referrerId = $referrerPartner?->id; $referrerRate = $referrerId ? self::DEFAULT_INDIVIDUAL_REFERRER_RATE : 0; } // 수당 계산 $partnerCommission = $baseAmount * ($partnerRate / 100); // 구독료 수당 (deposit에서만 1회 설정, balance에서는 0) $subscriptionFee = $contractProducts->sum('subscription_fee') ?? 0; $managerCommission = 0; if ($paymentType === SalesCommission::PAYMENT_DEPOSIT && $subscriptionFee > 0) { if ($management->manager_user_id) { // 매니저 지정 → 매니저 수당 $managerCommission = $subscriptionFee; } else { // 매니저 미지정 → 파트너 수당으로 편입 $partnerCommission += $subscriptionFee; } } $managerRate = 0; // 매니저는 비율 기반이 아님 $referrerCommission = ($referrerId && $referrerRate > 0) ? $baseAmount * ($referrerRate / 100) : 0; // 지급예정일 (익월 10일) $scheduledPaymentDate = SalesCommission::calculateScheduledPaymentDate($paymentDateCarbon); // 정산 생성 $commission = SalesCommission::create([ 'tenant_id' => $management->tenant_id, 'management_id' => $managementId, 'payment_type' => $paymentType, 'payment_amount' => $paymentAmount, 'payment_date' => $paymentDate, 'base_amount' => $baseAmount, 'partner_rate' => $partnerRate, 'manager_rate' => $managerRate, 'partner_commission' => $partnerCommission, 'manager_commission' => $managerCommission, 'scheduled_payment_date' => $scheduledPaymentDate, 'status' => SalesCommission::STATUS_PENDING, 'partner_id' => $partner->id, 'manager_user_id' => $management->manager_user_id, 'referrer_partner_id' => $referrerId, 'referrer_rate' => $referrerRate, 'referrer_commission' => $referrerCommission, ]); // 상품별 상세 내역 생성 foreach ($contractProducts as $contractProduct) { $productBaseAmount = ($contractProduct->registration_fee ?? 0) / 2; $productPartnerRate = $contractProduct->product->partner_commission ?? $partnerRate; $productManagerRate = $contractProduct->product->manager_commission ?? $managerRate; SalesCommissionDetail::create([ 'commission_id' => $commission->id, 'contract_product_id' => $contractProduct->id, 'registration_fee' => $contractProduct->registration_fee ?? 0, 'base_amount' => $productBaseAmount, 'partner_rate' => $productPartnerRate, 'manager_rate' => $productManagerRate, 'partner_commission' => $productBaseAmount * ($productPartnerRate / 100), 'manager_commission' => $productBaseAmount * ($productManagerRate / 100), ]); } // management 입금 정보 업데이트 $updateData = []; if ($paymentType === SalesCommission::PAYMENT_DEPOSIT) { $updateData = [ 'deposit_amount' => $paymentAmount, 'deposit_paid_date' => $paymentDate, 'deposit_status' => 'paid', ]; } else { $updateData = [ 'balance_amount' => $paymentAmount, 'balance_paid_date' => $paymentDate, 'balance_status' => 'paid', ]; } // 총 개발비 업데이트 $updateData['total_registration_fee'] = $totalRegistrationFee; $management->update($updateData); return $commission->load(['tenant', 'partner.user', 'manager', 'details']); }); } // ========================================================================= // 승인/지급 처리 // ========================================================================= /** * 승인 처리 */ public function approve(int $commissionId, int $approverId): SalesCommission { $commission = SalesCommission::findOrFail($commissionId); // 금액이 0이면 재계산하여 DB 업데이트 if ($commission->partner_commission <= 0) { $this->recalculateCommission($commission); } if (! $commission->approve($approverId)) { throw new \Exception('승인할 수 없는 상태입니다.'); } return $commission->fresh(['tenant', 'partner.user', 'manager', 'referrerPartner.user']); } /** * 일괄 승인 */ public function bulkApprove(array $ids, int $approverId): int { $count = 0; DB::transaction(function () use ($ids, $approverId, &$count) { $commissions = SalesCommission::whereIn('id', $ids) ->where('status', SalesCommission::STATUS_PENDING) ->get(); foreach ($commissions as $commission) { // 금액이 0이면 재계산 if ($commission->partner_commission <= 0) { $this->recalculateCommission($commission); } if ($commission->approve($approverId)) { $count++; } } }); return $count; } /** * 지급완료 처리 */ public function markAsPaid(int $commissionId, ?string $bankReference = null): SalesCommission { $commission = SalesCommission::findOrFail($commissionId); if (! $commission->markAsPaid($bankReference)) { throw new \Exception('지급완료 처리할 수 없는 상태입니다.'); } // 영업파트너 누적 수당 업데이트 $this->updatePartnerTotalCommission($commission->partner_id); return $commission->fresh(['tenant', 'partner.user', 'manager']); } /** * 일괄 지급완료 */ public function bulkMarkAsPaid(array $ids, ?string $bankReference = null): int { $count = 0; $partnerIds = []; DB::transaction(function () use ($ids, $bankReference, &$count, &$partnerIds) { $commissions = SalesCommission::whereIn('id', $ids) ->where('status', SalesCommission::STATUS_APPROVED) ->get(); foreach ($commissions as $commission) { if ($commission->markAsPaid($bankReference)) { $count++; $partnerIds[] = $commission->partner_id; } } }); // 영업파트너 누적 수당 일괄 업데이트 foreach (array_unique($partnerIds) as $partnerId) { $this->updatePartnerTotalCommission($partnerId); } return $count; } /** * 승인취소 처리 */ public function unapprove(int $commissionId): SalesCommission { $commission = SalesCommission::findOrFail($commissionId); if (! $commission->unapprove()) { throw new \Exception('승인취소할 수 없는 상태입니다.'); } return $commission->fresh(['tenant', 'partner.user', 'manager']); } /** * 취소 처리 */ public function cancel(int $commissionId): SalesCommission { $commission = SalesCommission::findOrFail($commissionId); if (! $commission->cancel()) { throw new \Exception('취소할 수 없는 상태입니다.'); } return $commission->fresh(['tenant', 'partner.user', 'manager']); } // ========================================================================= // 영업파트너/매니저 대시보드용 // ========================================================================= /** * 영업파트너 수당 요약 */ public function getPartnerCommissionSummary(int $partnerId): array { $commissions = SalesCommission::forPartner($partnerId)->get(); $thisMonth = now()->format('Y-m'); $thisMonthStart = now()->startOfMonth()->format('Y-m-d'); $thisMonthEnd = now()->endOfMonth()->format('Y-m-d'); // 1차/2차 수당 상세 계산 $firstCommissionDetails = $this->calculateStageCommission($commissions, 'first'); $secondCommissionDetails = $this->calculateStageCommission($commissions, 'second'); return [ // 이번 달 지급예정 (승인 완료된 건) 'scheduled_this_month' => $commissions ->where('status', SalesCommission::STATUS_APPROVED) ->filter(fn ($c) => $c->scheduled_payment_date->format('Y-m') === $thisMonth) ->sum('partner_commission'), // 누적 수령 수당 'total_received' => $commissions ->where('status', SalesCommission::STATUS_PAID) ->sum('partner_commission'), // 대기중 수당 'pending_amount' => $commissions ->where('status', SalesCommission::STATUS_PENDING) ->sum('partner_commission'), // 이번 달 신규 계약 건수 'contracts_this_month' => $commissions ->filter(fn ($c) => $c->payment_date >= $thisMonthStart && $c->payment_date <= $thisMonthEnd) ->count(), // 1차 수당 상세 'first_commission' => $firstCommissionDetails, // 2차 수당 상세 'second_commission' => $secondCommissionDetails, // 총 수당 금액 (1차 + 2차) 'total_commission' => $commissions->sum('partner_commission'), ]; } /** * 단계별 수당 계산 (1차/2차) * 파트너 수당의 50%씩 1차/2차로 분할 */ private function calculateStageCommission($commissions, string $stage): array { $paymentAtField = $stage === 'first' ? 'first_payment_at' : 'second_payment_at'; $paidAtField = $stage === 'first' ? 'first_partner_paid_at' : 'second_partner_paid_at'; $total = 0; $pending = 0; // 납입 대기 (입금 전) $scheduled = 0; // 지급예정 (입금 완료, 수당 미지급) $paid = 0; // 지급완료 foreach ($commissions as $commission) { // 파트너 수당의 50%가 각 단계별 금액 $stageAmount = $commission->partner_commission / 2; $total += $stageAmount; $paymentAt = $commission->{$paymentAtField}; $paidAt = $commission->{$paidAtField}; if ($paidAt) { // 지급완료 $paid += $stageAmount; } elseif ($paymentAt) { // 납입완료, 수당 지급예정 $scheduled += $stageAmount; } else { // 납입 대기 $pending += $stageAmount; } } return [ 'total' => $total, 'pending' => $pending, // 납입 대기 'scheduled' => $scheduled, // 지급예정 'paid' => $paid, // 지급완료 ]; } /** * 매니저 수당 요약 */ public function getManagerCommissionSummary(int $managerUserId): array { $commissions = SalesCommission::forManager($managerUserId)->get(); $thisMonth = now()->format('Y-m'); return [ // 이번 달 지급예정 (승인 완료된 건) 'scheduled_this_month' => $commissions ->where('status', SalesCommission::STATUS_APPROVED) ->filter(fn ($c) => $c->scheduled_payment_date->format('Y-m') === $thisMonth) ->sum('manager_commission'), // 누적 수령 수당 'total_received' => $commissions ->where('status', SalesCommission::STATUS_PAID) ->sum('manager_commission'), // 대기중 수당 'pending_amount' => $commissions ->where('status', SalesCommission::STATUS_PENDING) ->sum('manager_commission'), ]; } /** * 최근 수당 내역 (대시보드용) */ public function getRecentCommissions(int $partnerId, int $limit = 5): Collection { return SalesCommission::forPartner($partnerId) ->with(['tenant', 'management']) ->orderBy('created_at', 'desc') ->limit($limit) ->get(); } // ========================================================================= // 통계 // ========================================================================= /** * 정산 통계 (본사 대시보드용) */ public function getSettlementStats(int $year, int $month): array { $commissions = SalesCommission::forScheduledMonth($year, $month)->get(); return [ // 상태별 건수 및 금액 'pending' => [ 'count' => $commissions->where('status', SalesCommission::STATUS_PENDING)->count(), 'partner_total' => $commissions->where('status', SalesCommission::STATUS_PENDING)->sum('partner_commission'), 'manager_total' => $commissions->where('status', SalesCommission::STATUS_PENDING)->sum('manager_commission'), ], 'approved' => [ 'count' => $commissions->where('status', SalesCommission::STATUS_APPROVED)->count(), 'partner_total' => $commissions->where('status', SalesCommission::STATUS_APPROVED)->sum('partner_commission'), 'manager_total' => $commissions->where('status', SalesCommission::STATUS_APPROVED)->sum('manager_commission'), ], 'paid' => [ 'count' => $commissions->where('status', SalesCommission::STATUS_PAID)->count(), 'partner_total' => $commissions->where('status', SalesCommission::STATUS_PAID)->sum('partner_commission'), 'manager_total' => $commissions->where('status', SalesCommission::STATUS_PAID)->sum('manager_commission'), ], // 전체 합계 'total' => [ 'count' => $commissions->count(), 'base_amount' => $commissions->sum('base_amount'), 'partner_commission' => $commissions->sum('partner_commission'), 'manager_commission' => $commissions->sum('manager_commission'), ], ]; } /** * 정산 통계 (기간 범위) */ public function getSettlementStatsForRange(int $startYear, int $startMonth, int $endYear, int $endMonth): array { $startDate = \Carbon\Carbon::create($startYear, $startMonth, 1)->startOfMonth(); $endDate = \Carbon\Carbon::create($endYear, $endMonth, 1)->endOfMonth(); $commissions = SalesCommission::whereBetween('scheduled_payment_date', [$startDate, $endDate])->get(); return [ 'pending' => [ 'count' => $commissions->where('status', SalesCommission::STATUS_PENDING)->count(), 'partner_total' => $commissions->where('status', SalesCommission::STATUS_PENDING)->sum('partner_commission'), 'manager_total' => $commissions->where('status', SalesCommission::STATUS_PENDING)->sum('manager_commission'), ], 'approved' => [ 'count' => $commissions->where('status', SalesCommission::STATUS_APPROVED)->count(), 'partner_total' => $commissions->where('status', SalesCommission::STATUS_APPROVED)->sum('partner_commission'), 'manager_total' => $commissions->where('status', SalesCommission::STATUS_APPROVED)->sum('manager_commission'), ], 'paid' => [ 'count' => $commissions->where('status', SalesCommission::STATUS_PAID)->count(), 'partner_total' => $commissions->where('status', SalesCommission::STATUS_PAID)->sum('partner_commission'), 'manager_total' => $commissions->where('status', SalesCommission::STATUS_PAID)->sum('manager_commission'), ], 'total' => [ 'count' => $commissions->count(), 'base_amount' => $commissions->sum('base_amount'), 'partner_commission' => $commissions->sum('partner_commission'), 'manager_commission' => $commissions->sum('manager_commission'), ], ]; } /** * 입금 대기 중인 테넌트 목록 */ public function getPendingPaymentTenants(): Collection { return SalesTenantManagement::with(['tenant', 'salesPartner.user', 'manager']) ->contracted() ->where(function ($query) { $query->where('deposit_status', 'pending') ->orWhere('balance_status', 'pending'); }) ->orderBy('contracted_at', 'desc') ->get(); } // ========================================================================= // 내부 메서드 // ========================================================================= /** * 수당 재계산 (기존 0원 레코드 정상화) */ private function recalculateCommission(SalesCommission $commission): void { $management = SalesTenantManagement::with([ 'salesPartner.user.parent', 'contractProducts.product', 'tenantProspect.registeredBy.salesPartner', ])->find($commission->management_id); if (! $management) { return; } // 파트너 resolve (fallback: tenantProspect → registeredBy → salesPartner) $partner = $management->salesPartner; if (! $partner) { $partner = $management->tenantProspect?->registeredBy?->salesPartner; } if (! $partner) { return; } $contractProducts = $management->contractProducts; $totalRegistrationFee = $contractProducts->sum('registration_fee') ?: ($commission->payment_amount * 2); $baseAmount = $totalRegistrationFee / 2; $isGroup = $partner->isGroup(); // 파트너 수당 if ($isGroup) { $partnerRate = $partner->commission_rate ?? self::DEFAULT_GROUP_RATE; $referrerId = $partner->referrer_partner_id; $referrerRate = $referrerId ? self::DEFAULT_GROUP_REFERRER_RATE : 0; } else { $partnerRate = $partner->commission_rate ?? self::DEFAULT_PARTNER_RATE; $parentUser = $partner->user?->parent; $referrerPartner = $parentUser ? SalesPartner::where('user_id', $parentUser->id)->first() : null; $referrerId = $referrerPartner?->id; $referrerRate = $referrerId ? self::DEFAULT_INDIVIDUAL_REFERRER_RATE : 0; } $partnerCommission = $baseAmount * ($partnerRate / 100); // 구독료 수당 (deposit에서만 1회 설정, balance에서는 0) $subscriptionFee = $contractProducts->sum('subscription_fee') ?? 0; $managerCommission = 0; if ($commission->payment_type === SalesCommission::PAYMENT_DEPOSIT && $subscriptionFee > 0) { if ($management->manager_user_id) { $managerCommission = $subscriptionFee; } else { $partnerCommission += $subscriptionFee; } } // 유치수당 $referrerCommission = ($referrerId && $referrerRate > 0) ? $baseAmount * ($referrerRate / 100) : 0; // DB 업데이트 $updateData = [ 'base_amount' => $baseAmount, 'partner_rate' => $partnerRate, 'manager_rate' => 0, 'partner_commission' => $partnerCommission, 'manager_commission' => $managerCommission, 'referrer_rate' => $referrerRate, 'referrer_commission' => $referrerCommission, ]; // partner_id가 0이면 정상 값으로 교체 if ($commission->partner_id <= 0) { $updateData['partner_id'] = $partner->id; } // referrer_partner_id 설정 if ($referrerId) { $updateData['referrer_partner_id'] = $referrerId; } // payment_amount가 0이면 totalRegistrationFee/2로 설정 if ($commission->payment_amount <= 0) { $updateData['payment_amount'] = $baseAmount; } $commission->update($updateData); $commission->refresh(); } /** * 영업파트너 누적 수당 업데이트 */ private function updatePartnerTotalCommission(int $partnerId): void { $totalPaid = SalesCommission::forPartner($partnerId) ->paid() ->sum('partner_commission'); $contractCount = SalesCommission::forPartner($partnerId) ->paid() ->count(); SalesPartner::where('id', $partnerId)->update([ 'total_commission' => $totalPaid, 'total_contracts' => $contractCount, ]); } }