- TenantProspect 모델, 서비스, 컨트롤러 추가 - 명함 등록 시 2개월 영업권 부여 - 만료 후 1개월 쿨다운 기간 적용 - 테넌트 전환 기능 구현 - 사업자번호 중복 체크 API 추가 - 명함 이미지 업로드 지원 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
254 lines
8.5 KiB
PHP
254 lines
8.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Sales;
|
|
|
|
use App\Models\Sales\TenantProspect;
|
|
use App\Models\Tenants\Tenant;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
class TenantProspectService
|
|
{
|
|
/**
|
|
* 명함 등록 (영업권 확보)
|
|
*/
|
|
public function register(array $data, ?UploadedFile $businessCard = null): TenantProspect
|
|
{
|
|
return DB::transaction(function () use ($data, $businessCard) {
|
|
$now = now();
|
|
$expiresAt = $now->copy()->addMonths(TenantProspect::VALIDITY_MONTHS);
|
|
$cooldownEndsAt = $expiresAt->copy()->addMonths(TenantProspect::COOLDOWN_MONTHS);
|
|
|
|
// 명함 이미지 저장
|
|
$businessCardPath = null;
|
|
if ($businessCard) {
|
|
$businessCardPath = $this->uploadBusinessCard($businessCard, $data['registered_by']);
|
|
}
|
|
|
|
return TenantProspect::create([
|
|
'business_number' => $data['business_number'],
|
|
'company_name' => $data['company_name'],
|
|
'ceo_name' => $data['ceo_name'] ?? null,
|
|
'contact_phone' => $data['contact_phone'] ?? null,
|
|
'contact_email' => $data['contact_email'] ?? null,
|
|
'address' => $data['address'] ?? null,
|
|
'registered_by' => $data['registered_by'],
|
|
'business_card_path' => $businessCardPath,
|
|
'status' => TenantProspect::STATUS_ACTIVE,
|
|
'registered_at' => $now,
|
|
'expires_at' => $expiresAt,
|
|
'cooldown_ends_at' => $cooldownEndsAt,
|
|
'memo' => $data['memo'] ?? null,
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 영업권 정보 수정
|
|
*/
|
|
public function update(TenantProspect $prospect, array $data, ?UploadedFile $businessCard = null): TenantProspect
|
|
{
|
|
return DB::transaction(function () use ($prospect, $data, $businessCard) {
|
|
$updateData = [
|
|
'company_name' => $data['company_name'],
|
|
'ceo_name' => $data['ceo_name'] ?? null,
|
|
'contact_phone' => $data['contact_phone'] ?? null,
|
|
'contact_email' => $data['contact_email'] ?? null,
|
|
'address' => $data['address'] ?? null,
|
|
'memo' => $data['memo'] ?? null,
|
|
];
|
|
|
|
// 명함 이미지 교체
|
|
if ($businessCard) {
|
|
// 기존 이미지 삭제
|
|
if ($prospect->business_card_path) {
|
|
Storage::disk('tenant')->delete($prospect->business_card_path);
|
|
}
|
|
$updateData['business_card_path'] = $this->uploadBusinessCard($businessCard, $prospect->registered_by);
|
|
}
|
|
|
|
$prospect->update($updateData);
|
|
|
|
return $prospect->fresh();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 테넌트로 전환
|
|
*/
|
|
public function convertToTenant(TenantProspect $prospect, int $convertedBy): Tenant
|
|
{
|
|
return DB::transaction(function () use ($prospect, $convertedBy) {
|
|
// 테넌트 생성
|
|
$tenant = Tenant::create([
|
|
'company_name' => $prospect->company_name,
|
|
'business_num' => $prospect->business_number,
|
|
'ceo_name' => $prospect->ceo_name,
|
|
'phone' => $prospect->contact_phone,
|
|
'email' => $prospect->contact_email,
|
|
'address' => $prospect->address,
|
|
'tenant_st_code' => 'trial',
|
|
'tenant_type' => 'customer',
|
|
'created_by' => $convertedBy,
|
|
]);
|
|
|
|
// 영업권 상태 업데이트
|
|
$prospect->update([
|
|
'status' => TenantProspect::STATUS_CONVERTED,
|
|
'tenant_id' => $tenant->id,
|
|
'converted_at' => now(),
|
|
'converted_by' => $convertedBy,
|
|
]);
|
|
|
|
return $tenant;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 영업권 만료 처리 (배치용)
|
|
*/
|
|
public function expireOldProspects(): int
|
|
{
|
|
return TenantProspect::where('status', TenantProspect::STATUS_ACTIVE)
|
|
->where('expires_at', '<=', now())
|
|
->update(['status' => TenantProspect::STATUS_EXPIRED]);
|
|
}
|
|
|
|
/**
|
|
* 사업자번호로 등록 가능 여부 확인
|
|
*/
|
|
public function canRegister(string $businessNumber, ?int $excludeId = null): array
|
|
{
|
|
$query = TenantProspect::where('business_number', $businessNumber);
|
|
|
|
if ($excludeId) {
|
|
$query->where('id', '!=', $excludeId);
|
|
}
|
|
|
|
// 이미 전환된 경우
|
|
$converted = (clone $query)->where('status', TenantProspect::STATUS_CONVERTED)->first();
|
|
if ($converted) {
|
|
return [
|
|
'can_register' => false,
|
|
'reason' => '이미 테넌트로 전환된 회사입니다.',
|
|
'prospect' => $converted,
|
|
];
|
|
}
|
|
|
|
// 유효한 영업권이 있는 경우
|
|
$active = (clone $query)->active()->first();
|
|
if ($active) {
|
|
return [
|
|
'can_register' => false,
|
|
'reason' => "이미 {$active->registeredBy->name}님이 영업권을 보유 중입니다. (만료: {$active->expires_at->format('Y-m-d')})",
|
|
'prospect' => $active,
|
|
];
|
|
}
|
|
|
|
// 쿨다운 중인 경우
|
|
$inCooldown = (clone $query)
|
|
->where('status', TenantProspect::STATUS_EXPIRED)
|
|
->where('cooldown_ends_at', '>', now())
|
|
->first();
|
|
|
|
if ($inCooldown) {
|
|
return [
|
|
'can_register' => false,
|
|
'reason' => "쿨다운 기간 중입니다. (등록 가능: {$inCooldown->cooldown_ends_at->format('Y-m-d')})",
|
|
'prospect' => $inCooldown,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'can_register' => true,
|
|
'reason' => null,
|
|
'prospect' => null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 목록 조회
|
|
*/
|
|
public function getProspects(array $filters = [])
|
|
{
|
|
$query = TenantProspect::with(['registeredBy', 'tenant']);
|
|
|
|
// 검색
|
|
if (!empty($filters['search'])) {
|
|
$search = $filters['search'];
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('company_name', 'like', "%{$search}%")
|
|
->orWhere('business_number', 'like', "%{$search}%")
|
|
->orWhere('ceo_name', 'like', "%{$search}%")
|
|
->orWhere('contact_phone', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// 상태 필터
|
|
if (!empty($filters['status'])) {
|
|
if ($filters['status'] === 'active') {
|
|
$query->active();
|
|
} elseif ($filters['status'] === 'expired') {
|
|
$query->where('status', TenantProspect::STATUS_EXPIRED);
|
|
} elseif ($filters['status'] === 'converted') {
|
|
$query->converted();
|
|
}
|
|
}
|
|
|
|
// 특정 영업파트너
|
|
if (!empty($filters['registered_by'])) {
|
|
$query->byPartner($filters['registered_by']);
|
|
}
|
|
|
|
return $query->orderBy('created_at', 'desc');
|
|
}
|
|
|
|
/**
|
|
* 통계 조회
|
|
*/
|
|
public function getStats(?int $partnerId = null): array
|
|
{
|
|
$baseQuery = TenantProspect::query();
|
|
|
|
if ($partnerId) {
|
|
$baseQuery->byPartner($partnerId);
|
|
}
|
|
|
|
return [
|
|
'total' => (clone $baseQuery)->count(),
|
|
'active' => (clone $baseQuery)->active()->count(),
|
|
'expired' => (clone $baseQuery)->where('status', TenantProspect::STATUS_EXPIRED)->count(),
|
|
'converted' => (clone $baseQuery)->converted()->count(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 명함 이미지 업로드
|
|
*/
|
|
private function uploadBusinessCard(UploadedFile $file, int $userId): string
|
|
{
|
|
$storedName = Str::uuid() . '.' . $file->getClientOriginalExtension();
|
|
$filePath = "prospects/{$userId}/{$storedName}";
|
|
|
|
Storage::disk('tenant')->put($filePath, file_get_contents($file));
|
|
|
|
return $filePath;
|
|
}
|
|
|
|
/**
|
|
* 명함 이미지 삭제
|
|
*/
|
|
public function deleteBusinessCard(TenantProspect $prospect): bool
|
|
{
|
|
if ($prospect->business_card_path && Storage::disk('tenant')->exists($prospect->business_card_path)) {
|
|
Storage::disk('tenant')->delete($prospect->business_card_path);
|
|
$prospect->update(['business_card_path' => null]);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|