영업 관련 코드 및 문서 전체에서 "가입비"를 "개발비"로 변경 - 컨트롤러, 서비스, 모델 - 뷰 템플릿 (blade 파일) - 가이드북 문서 (마크다운) - 설정 파일 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
505 lines
18 KiB
PHP
505 lines
18 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Sales\SalesCommission;
|
|
use App\Models\Sales\SalesCommissionDetail;
|
|
use App\Models\Sales\SalesContractProduct;
|
|
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_MANAGER_RATE = 5.00;
|
|
|
|
// =========================================================================
|
|
// 정산 목록 조회
|
|
// =========================================================================
|
|
|
|
/**
|
|
* 정산 목록 조회 (페이지네이션)
|
|
*/
|
|
public function getCommissions(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
|
{
|
|
$query = SalesCommission::query()
|
|
->with(['tenant', 'partner.user', 'manager', 'management']);
|
|
|
|
// 상태 필터
|
|
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_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['search'])) {
|
|
$search = $filters['search'];
|
|
$query->whereHas('tenant', function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('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', 'contractProducts.product'])
|
|
->findOrFail($managementId);
|
|
|
|
// 영업파트너 필수 체크
|
|
if (!$management->sales_partner_id) {
|
|
throw new \Exception('영업파트너가 지정되지 않았습니다.');
|
|
}
|
|
|
|
$partner = $management->salesPartner;
|
|
$paymentDateCarbon = Carbon::parse($paymentDate);
|
|
|
|
// 계약 상품이 없으면 기본 계산
|
|
$contractProducts = $management->contractProducts;
|
|
$totalRegistrationFee = $contractProducts->sum('registration_fee') ?: $paymentAmount * 2;
|
|
$baseAmount = $totalRegistrationFee / 2; // 개발비의 50%
|
|
|
|
// 수당률 (영업파트너 설정 또는 기본값)
|
|
$partnerRate = $partner->commission_rate ?? self::DEFAULT_PARTNER_RATE;
|
|
$managerRate = $partner->manager_commission_rate ?? self::DEFAULT_MANAGER_RATE;
|
|
|
|
// 수당 계산
|
|
$partnerCommission = $baseAmount * ($partnerRate / 100);
|
|
$managerCommission = $management->manager_user_id
|
|
? $baseAmount * ($managerRate / 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,
|
|
]);
|
|
|
|
// 상품별 상세 내역 생성
|
|
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);
|
|
|
|
if (!$commission->approve($approverId)) {
|
|
throw new \Exception('승인할 수 없는 상태입니다.');
|
|
}
|
|
|
|
return $commission->fresh(['tenant', 'partner.user', 'manager']);
|
|
}
|
|
|
|
/**
|
|
* 일괄 승인
|
|
*/
|
|
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) {
|
|
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 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 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();
|
|
}
|
|
|
|
// =========================================================================
|
|
// 내부 메서드
|
|
// =========================================================================
|
|
|
|
/**
|
|
* 영업파트너 누적 수당 업데이트
|
|
*/
|
|
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,
|
|
]);
|
|
}
|
|
}
|