379 lines
10 KiB
PHP
379 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Models\Sales;
|
|
|
|
use App\Models\Tenants\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
/**
|
|
* 테넌트별 영업 관리 모델 (tenants 외래키 연결)
|
|
*
|
|
* @property int $id
|
|
* @property int $tenant_id
|
|
* @property int|null $sales_partner_id
|
|
* @property int|null $manager_user_id
|
|
* @property int $sales_scenario_step
|
|
* @property int $manager_scenario_step
|
|
* @property string $status
|
|
* @property \Carbon\Carbon|null $first_contact_at
|
|
* @property \Carbon\Carbon|null $contracted_at
|
|
* @property \Carbon\Carbon|null $onboarding_completed_at
|
|
* @property float|null $membership_fee
|
|
* @property \Carbon\Carbon|null $membership_paid_at
|
|
* @property string|null $membership_status
|
|
* @property float|null $sales_commission
|
|
* @property float|null $manager_commission
|
|
* @property \Carbon\Carbon|null $commission_paid_at
|
|
* @property string|null $commission_status
|
|
* @property int $sales_progress
|
|
* @property int $manager_progress
|
|
* @property string|null $notes
|
|
*/
|
|
class SalesTenantManagement extends Model
|
|
{
|
|
use SoftDeletes;
|
|
|
|
protected $table = 'sales_tenant_managements';
|
|
|
|
protected $fillable = [
|
|
'tenant_id',
|
|
'tenant_prospect_id',
|
|
'sales_partner_id',
|
|
'manager_user_id',
|
|
'sales_scenario_step',
|
|
'manager_scenario_step',
|
|
'status',
|
|
'first_contact_at',
|
|
'contracted_at',
|
|
'onboarding_completed_at',
|
|
'membership_fee',
|
|
'membership_paid_at',
|
|
'membership_status',
|
|
'sales_commission',
|
|
'manager_commission',
|
|
'commission_paid_at',
|
|
'commission_status',
|
|
'sales_progress',
|
|
'manager_progress',
|
|
'hq_status',
|
|
'handover_at',
|
|
'incentive_status',
|
|
'notes',
|
|
// 입금 정보
|
|
'deposit_amount',
|
|
'deposit_paid_date',
|
|
'deposit_status',
|
|
'balance_amount',
|
|
'balance_paid_date',
|
|
'balance_status',
|
|
'total_registration_fee',
|
|
];
|
|
|
|
protected $casts = [
|
|
'sales_scenario_step' => 'integer',
|
|
'manager_scenario_step' => 'integer',
|
|
'membership_fee' => 'decimal:2',
|
|
'sales_commission' => 'decimal:2',
|
|
'manager_commission' => 'decimal:2',
|
|
'sales_progress' => 'integer',
|
|
'manager_progress' => 'integer',
|
|
'first_contact_at' => 'datetime',
|
|
'contracted_at' => 'datetime',
|
|
'onboarding_completed_at' => 'datetime',
|
|
'membership_paid_at' => 'datetime',
|
|
'commission_paid_at' => 'datetime',
|
|
'handover_at' => 'datetime',
|
|
// 입금 정보
|
|
'deposit_amount' => 'decimal:2',
|
|
'deposit_paid_date' => 'date',
|
|
'balance_amount' => 'decimal:2',
|
|
'balance_paid_date' => 'date',
|
|
'total_registration_fee' => 'decimal:2',
|
|
];
|
|
|
|
/**
|
|
* 상태 상수
|
|
*/
|
|
const STATUS_PROSPECT = 'prospect';
|
|
|
|
const STATUS_APPROACH = 'approach';
|
|
|
|
const STATUS_NEGOTIATION = 'negotiation';
|
|
|
|
const STATUS_CONTRACTED = 'contracted';
|
|
|
|
const STATUS_ONBOARDING = 'onboarding';
|
|
|
|
const STATUS_ACTIVE = 'active';
|
|
|
|
const STATUS_CHURNED = 'churned';
|
|
|
|
/**
|
|
* 상태 라벨
|
|
*/
|
|
public static array $statusLabels = [
|
|
self::STATUS_PROSPECT => '잠재 고객',
|
|
self::STATUS_APPROACH => '접근 중',
|
|
self::STATUS_NEGOTIATION => '협상 중',
|
|
self::STATUS_CONTRACTED => '계약 완료',
|
|
self::STATUS_ONBOARDING => '온보딩 중',
|
|
self::STATUS_ACTIVE => '활성 고객',
|
|
self::STATUS_CHURNED => '이탈',
|
|
];
|
|
|
|
/**
|
|
* 본사 진행 상태 상수
|
|
*/
|
|
const HQ_STATUS_PENDING = 'pending'; // 대기
|
|
|
|
const HQ_STATUS_REVIEW = 'review'; // 검토
|
|
|
|
const HQ_STATUS_PLANNING = 'planning'; // 기획안작성
|
|
|
|
const HQ_STATUS_CODING = 'coding'; // 개발코드작성
|
|
|
|
const HQ_STATUS_DEV_TEST = 'dev_test'; // 개발테스트
|
|
|
|
const HQ_STATUS_DEV_DONE = 'dev_done'; // 개발완료
|
|
|
|
const HQ_STATUS_INT_TEST = 'int_test'; // 통합테스트
|
|
|
|
const HQ_STATUS_HANDOVER = 'handover'; // 인계
|
|
|
|
/**
|
|
* 본사 진행 상태 라벨
|
|
*/
|
|
public static array $hqStatusLabels = [
|
|
self::HQ_STATUS_PENDING => '대기',
|
|
self::HQ_STATUS_REVIEW => '검토',
|
|
self::HQ_STATUS_PLANNING => '기획안작성',
|
|
self::HQ_STATUS_CODING => '개발코드작성',
|
|
self::HQ_STATUS_DEV_TEST => '개발테스트',
|
|
self::HQ_STATUS_DEV_DONE => '개발완료',
|
|
self::HQ_STATUS_INT_TEST => '통합테스트',
|
|
self::HQ_STATUS_HANDOVER => '인계',
|
|
];
|
|
|
|
/**
|
|
* 본사 진행 상태 순서 (프로그레스바용)
|
|
*/
|
|
public static array $hqStatusOrder = [
|
|
self::HQ_STATUS_PENDING => 0,
|
|
self::HQ_STATUS_REVIEW => 1,
|
|
self::HQ_STATUS_PLANNING => 2,
|
|
self::HQ_STATUS_CODING => 3,
|
|
self::HQ_STATUS_DEV_TEST => 4,
|
|
self::HQ_STATUS_DEV_DONE => 5,
|
|
self::HQ_STATUS_INT_TEST => 6,
|
|
self::HQ_STATUS_HANDOVER => 7,
|
|
];
|
|
|
|
/**
|
|
* 수당 지급 상태 상수
|
|
*/
|
|
const INCENTIVE_PENDING = 'pending'; // 대기
|
|
|
|
const INCENTIVE_ELIGIBLE = 'eligible'; // 지급대상
|
|
|
|
const INCENTIVE_PAID = 'paid'; // 지급완료
|
|
|
|
/**
|
|
* 수당 지급 상태 라벨
|
|
*/
|
|
public static array $incentiveStatusLabels = [
|
|
self::INCENTIVE_PENDING => '대기',
|
|
self::INCENTIVE_ELIGIBLE => '지급대상',
|
|
self::INCENTIVE_PAID => '지급완료',
|
|
];
|
|
|
|
/**
|
|
* 테넌트 관계
|
|
*/
|
|
public function tenant(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Tenant::class);
|
|
}
|
|
|
|
/**
|
|
* 가망고객 관계
|
|
*/
|
|
public function tenantProspect(): BelongsTo
|
|
{
|
|
return $this->belongsTo(TenantProspect::class, 'tenant_prospect_id');
|
|
}
|
|
|
|
/**
|
|
* 영업 담당자 관계
|
|
*/
|
|
public function salesPartner(): BelongsTo
|
|
{
|
|
return $this->belongsTo(SalesPartner::class, 'sales_partner_id');
|
|
}
|
|
|
|
/**
|
|
* 관리 매니저 관계
|
|
*/
|
|
public function manager(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'manager_user_id');
|
|
}
|
|
|
|
/**
|
|
* 체크리스트 관계
|
|
*/
|
|
public function checklists(): HasMany
|
|
{
|
|
return $this->hasMany(SalesScenarioChecklist::class, 'tenant_id', 'tenant_id');
|
|
}
|
|
|
|
/**
|
|
* 상담 기록 관계
|
|
*/
|
|
public function consultations(): HasMany
|
|
{
|
|
return $this->hasMany(SalesConsultation::class, 'tenant_id', 'tenant_id');
|
|
}
|
|
|
|
/**
|
|
* 수수료 정산 관계
|
|
*/
|
|
public function commissions(): HasMany
|
|
{
|
|
return $this->hasMany(SalesCommission::class, 'management_id');
|
|
}
|
|
|
|
/**
|
|
* 계약 상품 관계
|
|
*/
|
|
public function contractProducts(): HasMany
|
|
{
|
|
return $this->hasMany(SalesContractProduct::class, 'management_id');
|
|
}
|
|
|
|
/**
|
|
* 테넌트 ID로 조회 또는 생성
|
|
*/
|
|
public static function findOrCreateByTenant(int $tenantId): self
|
|
{
|
|
return self::firstOrCreate(
|
|
['tenant_id' => $tenantId],
|
|
[
|
|
'status' => self::STATUS_PROSPECT,
|
|
'sales_scenario_step' => 1,
|
|
'manager_scenario_step' => 1,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 가망고객 ID로 조회 또는 생성
|
|
*/
|
|
public static function findOrCreateByProspect(int $prospectId): self
|
|
{
|
|
return self::firstOrCreate(
|
|
['tenant_prospect_id' => $prospectId],
|
|
[
|
|
'status' => self::STATUS_PROSPECT,
|
|
'sales_scenario_step' => 1,
|
|
'manager_scenario_step' => 1,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 테넌트 또는 가망고객 ID로 조회 또는 생성
|
|
*/
|
|
public static function findOrCreateByTenantOrProspect(?int $tenantId, ?int $prospectId): self
|
|
{
|
|
if ($tenantId) {
|
|
return self::findOrCreateByTenant($tenantId);
|
|
}
|
|
if ($prospectId) {
|
|
return self::findOrCreateByProspect($prospectId);
|
|
}
|
|
throw new \InvalidArgumentException('tenant_id 또는 prospect_id 중 하나는 필수입니다.');
|
|
}
|
|
|
|
/**
|
|
* 진행률 업데이트
|
|
*/
|
|
public function updateProgress(string $scenarioType, int $progress): void
|
|
{
|
|
$field = $scenarioType === 'sales' ? 'sales_progress' : 'manager_progress';
|
|
$this->update([$field => $progress]);
|
|
}
|
|
|
|
/**
|
|
* 현재 단계 업데이트
|
|
*/
|
|
public function updateStep(string $scenarioType, int $step): void
|
|
{
|
|
$field = $scenarioType === 'sales' ? 'sales_scenario_step' : 'manager_scenario_step';
|
|
$this->update([$field => $step]);
|
|
}
|
|
|
|
/**
|
|
* 상태 라벨 Accessor
|
|
*/
|
|
public function getStatusLabelAttribute(): string
|
|
{
|
|
return self::$statusLabels[$this->status] ?? $this->status;
|
|
}
|
|
|
|
/**
|
|
* 본사 진행 상태 라벨 Accessor
|
|
*/
|
|
public function getHqStatusLabelAttribute(): string
|
|
{
|
|
return self::$hqStatusLabels[$this->hq_status ?? self::HQ_STATUS_PENDING] ?? '대기';
|
|
}
|
|
|
|
/**
|
|
* 본사 진행 상태 순서 (0-7)
|
|
*/
|
|
public function getHqStatusStepAttribute(): int
|
|
{
|
|
return self::$hqStatusOrder[$this->hq_status ?? self::HQ_STATUS_PENDING] ?? 0;
|
|
}
|
|
|
|
/**
|
|
* 수당 지급 상태 라벨 Accessor
|
|
*/
|
|
public function getIncentiveStatusLabelAttribute(): string
|
|
{
|
|
return self::$incentiveStatusLabels[$this->incentive_status ?? self::INCENTIVE_PENDING] ?? '대기';
|
|
}
|
|
|
|
/**
|
|
* 본사 진행 가능 여부 (매니저 100% 완료 시)
|
|
*/
|
|
public function isHqProgressEnabled(): bool
|
|
{
|
|
return $this->manager_progress >= 100;
|
|
}
|
|
|
|
/**
|
|
* 특정 상태 스코프
|
|
*/
|
|
public function scopeByStatus($query, string $status)
|
|
{
|
|
return $query->where('status', $status);
|
|
}
|
|
|
|
/**
|
|
* 계약 완료 스코프
|
|
*/
|
|
public function scopeContracted($query)
|
|
{
|
|
return $query->whereIn('status', [
|
|
self::STATUS_CONTRACTED,
|
|
self::STATUS_ONBOARDING,
|
|
self::STATUS_ACTIVE,
|
|
]);
|
|
}
|
|
}
|