Files
sam-manage/app/Services/SalesCommissionService.php
2026-02-25 11:45:01 +09:00

715 lines
28 KiB
PHP

<?php
namespace App\Services;
use App\Models\Sales\SalesCommission;
use App\Models\Sales\SalesCommissionDetail;
use App\Models\Sales\SalesPartner;
use App\Models\Sales\SalesTenantManagement;
use Carbon\Carbon;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class SalesCommissionService
{
/**
* 기본 수당률
*/
const DEFAULT_PARTNER_RATE = 20.00;
const DEFAULT_GROUP_RATE = 30.00; // 단체 파트너 수당률
const DEFAULT_INDIVIDUAL_REFERRER_RATE = 5.00; // 개인 유치수당률
const DEFAULT_GROUP_REFERRER_RATE = 3.00; // 단체 유치수당률
// =========================================================================
// 정산 목록 조회
// =========================================================================
/**
* 정산 목록 조회 (페이지네이션)
*/
public function getCommissions(array $filters = [], int $perPage = 20): LengthAwarePaginator
{
$query = SalesCommission::query()
->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);
// 매니저 수당 = 구독료 1개월 (비율 아님)
$subscriptionFee = $contractProducts->sum('subscription_fee') ?? 0;
$managerCommission = $management->manager_user_id ? $subscriptionFee : 0;
$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);
// 매니저 수당 = 구독료 1개월
$subscriptionFee = $contractProducts->sum('subscription_fee') ?? 0;
$managerCommission = $management->manager_user_id ? $subscriptionFee : 0;
// 유치수당
$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,
]);
}
}