- tenants.tenant_type ENUM 확장: DEMO_SHOWCASE, DEMO_PARTNER, DEMO_TRIAL - demo_expires_at, demo_source_partner_id 컬럼 추가 - Tenant 모델에 데모 관련 메서드 추가 (isDemoTenant, isDemoShowcase 등) - getOption/setOption 헬퍼 메서드 추가 - 데모→정식 전환 convertToProduction() 메서드
406 lines
9.5 KiB
PHP
406 lines
9.5 KiB
PHP
<?php
|
|
|
|
namespace App\Models\Tenants;
|
|
|
|
use App\Models\Commons\File;
|
|
use App\Models\Members\User;
|
|
use App\Models\Members\UserRole;
|
|
use App\Models\Members\UserTenant;
|
|
use App\Models\Permissions\Role;
|
|
use App\Traits\ModelTrait;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
/**
|
|
* @mixin IdeHelperTenant
|
|
*/
|
|
class Tenant extends Model
|
|
{
|
|
use ModelTrait, SoftDeletes;
|
|
|
|
// 데모 테넌트 유형 상수
|
|
const TYPE_STD = 'STD';
|
|
|
|
const TYPE_TPL = 'TPL';
|
|
|
|
const TYPE_HQ = 'HQ';
|
|
|
|
const TYPE_DEMO_SHOWCASE = 'DEMO_SHOWCASE';
|
|
|
|
const TYPE_DEMO_PARTNER = 'DEMO_PARTNER';
|
|
|
|
const TYPE_DEMO_TRIAL = 'DEMO_TRIAL';
|
|
|
|
const DEMO_TYPES = [
|
|
self::TYPE_DEMO_SHOWCASE,
|
|
self::TYPE_DEMO_PARTNER,
|
|
self::TYPE_DEMO_TRIAL,
|
|
];
|
|
|
|
// 데모 options 키 상수
|
|
const OPTION_DEMO_PRESET = 'demo_preset';
|
|
|
|
const OPTION_DEMO_LIMITS = 'demo_limits';
|
|
|
|
const OPTION_DEMO_READ_ONLY = 'demo_read_only';
|
|
|
|
protected $fillable = [
|
|
'company_name',
|
|
'code',
|
|
'email',
|
|
'phone',
|
|
'address',
|
|
'business_num',
|
|
'corp_reg_no',
|
|
'ceo_name',
|
|
'homepage',
|
|
'fax',
|
|
'logo',
|
|
'admin_memo',
|
|
'options',
|
|
'tenant_st_code',
|
|
'billing_tp_code',
|
|
'tenant_type',
|
|
'demo_expires_at',
|
|
'demo_source_partner_id',
|
|
];
|
|
|
|
protected $guarded = [
|
|
'id',
|
|
'created_at',
|
|
'updated_at',
|
|
'deleted_at',
|
|
'plan_id',
|
|
'subscription_id',
|
|
];
|
|
|
|
protected $casts = [
|
|
'trial_ends_at' => 'datetime',
|
|
'demo_expires_at' => 'datetime',
|
|
'expires_at' => 'datetime',
|
|
'last_paid_at' => 'datetime',
|
|
'max_users' => 'integer',
|
|
'options' => 'array',
|
|
'created_at' => 'datetime',
|
|
'updated_at' => 'datetime',
|
|
'deleted_at' => 'datetime',
|
|
];
|
|
|
|
protected $hidden = [
|
|
'deleted_at',
|
|
];
|
|
|
|
/**
|
|
* 활성화된 테넌트만 조회하는 스코프
|
|
*/
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->whereIn('tenant_st_code', ['trial', 'active']);
|
|
}
|
|
|
|
/**
|
|
* 테넌트가 활성 상태인지 확인
|
|
*/
|
|
public function isActive(): bool
|
|
{
|
|
return in_array($this->tenant_st_code, ['trial', 'active']);
|
|
}
|
|
|
|
/**
|
|
* 테넌트가 트라이얼 상태인지 확인
|
|
*/
|
|
public function isTrial(): bool
|
|
{
|
|
return $this->tenant_st_code === 'trial';
|
|
}
|
|
|
|
// 관계 정의 (예시)
|
|
public function plan()
|
|
{
|
|
return $this->belongsTo(Plan::class, 'plan_id');
|
|
}
|
|
|
|
public function subscription()
|
|
{
|
|
return $this->belongsTo(Subscription::class, 'subscription_id');
|
|
}
|
|
|
|
public function userTenants()
|
|
{
|
|
return $this->hasMany(UserTenant::class);
|
|
}
|
|
|
|
public function users()
|
|
{
|
|
return $this->belongsToMany(User::class, 'user_tenants');
|
|
}
|
|
|
|
public function roles()
|
|
{
|
|
return $this->hasMany(Role::class);
|
|
}
|
|
|
|
public function userRoles()
|
|
{
|
|
return $this->hasMany(UserRole::class);
|
|
}
|
|
|
|
public function files()
|
|
{
|
|
return $this->morphMany(File::class, 'fileable');
|
|
}
|
|
|
|
/**
|
|
* Get storage usage percentage
|
|
*/
|
|
public function getStorageUsagePercentage(): float
|
|
{
|
|
if (! $this->storage_limit || $this->storage_limit == 0) {
|
|
return 0;
|
|
}
|
|
|
|
return ($this->storage_used / $this->storage_limit) * 100;
|
|
}
|
|
|
|
/**
|
|
* Check if storage is near limit (90%)
|
|
*/
|
|
public function isStorageNearLimit(): bool
|
|
{
|
|
return $this->getStorageUsagePercentage() >= 90;
|
|
}
|
|
|
|
/**
|
|
* Check if storage quota exceeded
|
|
*/
|
|
public function isStorageExceeded(): bool
|
|
{
|
|
return $this->storage_used > $this->storage_limit;
|
|
}
|
|
|
|
/**
|
|
* Check if in grace period
|
|
*/
|
|
public function isInGracePeriod(): bool
|
|
{
|
|
return $this->storage_grace_period_until && now()->lessThan($this->storage_grace_period_until);
|
|
}
|
|
|
|
/**
|
|
* Check if upload is allowed
|
|
*/
|
|
public function canUpload(int $fileSize = 0): array
|
|
{
|
|
$newUsage = $this->storage_used + $fileSize;
|
|
|
|
// Check if exceeds limit
|
|
if ($newUsage > $this->storage_limit) {
|
|
// Check grace period
|
|
if ($this->isInGracePeriod()) {
|
|
return [
|
|
'allowed' => true,
|
|
'warning' => true,
|
|
'message' => __('file.storage_exceeded_grace_period', [
|
|
'until' => $this->storage_grace_period_until->format('Y-m-d'),
|
|
]),
|
|
];
|
|
}
|
|
|
|
// Grace period expired - block upload
|
|
return [
|
|
'allowed' => false,
|
|
'message' => __('file.storage_quota_exceeded'),
|
|
];
|
|
}
|
|
|
|
// Check if near limit (90%)
|
|
$percentage = ($newUsage / $this->storage_limit) * 100;
|
|
if ($percentage >= 90 && ! $this->storage_warning_sent_at) {
|
|
// Send warning (once)
|
|
$this->update([
|
|
'storage_warning_sent_at' => now(),
|
|
'storage_grace_period_until' => now()->addDays(7),
|
|
]);
|
|
|
|
// TODO: Dispatch email notification
|
|
// dispatch(new SendStorageWarningEmail($this));
|
|
}
|
|
|
|
return ['allowed' => true];
|
|
}
|
|
|
|
/**
|
|
* Increment storage usage
|
|
*/
|
|
public function incrementStorage(int $bytes): void
|
|
{
|
|
$this->increment('storage_used', $bytes);
|
|
}
|
|
|
|
/**
|
|
* Decrement storage usage
|
|
*/
|
|
public function decrementStorage(int $bytes): void
|
|
{
|
|
$this->decrement('storage_used', max(0, $bytes));
|
|
}
|
|
|
|
/**
|
|
* Get human-readable storage used
|
|
*/
|
|
public function getStorageUsedFormatted(): string
|
|
{
|
|
return $this->formatBytes($this->storage_used);
|
|
}
|
|
|
|
/**
|
|
* Get human-readable storage limit
|
|
*/
|
|
public function getStorageLimitFormatted(): string
|
|
{
|
|
return $this->formatBytes($this->storage_limit);
|
|
}
|
|
|
|
/**
|
|
* Format bytes to human-readable string
|
|
*/
|
|
private function formatBytes(int $bytes): string
|
|
{
|
|
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
$bytes = max($bytes, 0);
|
|
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
|
$pow = min($pow, count($units) - 1);
|
|
$bytes /= (1 << (10 * $pow));
|
|
|
|
return round($bytes, 2).' '.$units[$pow];
|
|
}
|
|
|
|
// ─── options 헬퍼 메서드 ───
|
|
|
|
public function getOption(string $key, mixed $default = null): mixed
|
|
{
|
|
return data_get($this->options, $key, $default);
|
|
}
|
|
|
|
public function setOption(string $key, mixed $value): self
|
|
{
|
|
$options = $this->options ?? [];
|
|
data_set($options, $key, $value);
|
|
$this->options = $options;
|
|
|
|
return $this;
|
|
}
|
|
|
|
// ─── 데모 테넌트 관련 메서드 ───
|
|
|
|
/**
|
|
* 데모 테넌트인지 확인 (모든 데모 유형)
|
|
*/
|
|
public function isDemoTenant(): bool
|
|
{
|
|
return in_array($this->tenant_type, self::DEMO_TYPES);
|
|
}
|
|
|
|
/**
|
|
* 데모 쇼케이스(공용 읽기전용)인지 확인
|
|
*/
|
|
public function isDemoShowcase(): bool
|
|
{
|
|
return $this->tenant_type === self::TYPE_DEMO_SHOWCASE;
|
|
}
|
|
|
|
/**
|
|
* 파트너 데모인지 확인
|
|
*/
|
|
public function isDemoPartner(): bool
|
|
{
|
|
return $this->tenant_type === self::TYPE_DEMO_PARTNER;
|
|
}
|
|
|
|
/**
|
|
* 고객 체험 데모인지 확인
|
|
*/
|
|
public function isDemoTrial(): bool
|
|
{
|
|
return $this->tenant_type === self::TYPE_DEMO_TRIAL;
|
|
}
|
|
|
|
/**
|
|
* 데모 만료 여부 확인
|
|
*/
|
|
public function isDemoExpired(): bool
|
|
{
|
|
if (! $this->isDemoTenant()) {
|
|
return false;
|
|
}
|
|
|
|
// 쇼케이스는 만료 없음
|
|
if ($this->isDemoShowcase()) {
|
|
return false;
|
|
}
|
|
|
|
return $this->demo_expires_at && now()->gt($this->demo_expires_at);
|
|
}
|
|
|
|
/**
|
|
* 데모 읽기전용인지 확인
|
|
*/
|
|
public function isDemoReadOnly(): bool
|
|
{
|
|
return $this->isDemoShowcase()
|
|
|| $this->getOption(self::OPTION_DEMO_READ_ONLY, false);
|
|
}
|
|
|
|
/**
|
|
* 데모 테넌트만 조회하는 스코프
|
|
*/
|
|
public function scopeDemo($query)
|
|
{
|
|
return $query->whereIn('tenant_type', self::DEMO_TYPES);
|
|
}
|
|
|
|
/**
|
|
* 데모 프리셋 조회
|
|
*/
|
|
public function getDemoPreset(): ?string
|
|
{
|
|
return $this->getOption(self::OPTION_DEMO_PRESET);
|
|
}
|
|
|
|
/**
|
|
* 데모 수량 제한 조회
|
|
*/
|
|
public function getDemoLimits(): array
|
|
{
|
|
return $this->getOption(self::OPTION_DEMO_LIMITS, [
|
|
'max_items' => 100,
|
|
'max_orders' => 50,
|
|
'max_productions' => 30,
|
|
'max_users' => 5,
|
|
'max_storage_gb' => 1,
|
|
'max_ai_tokens' => 100000,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 데모 → 정식 테넌트로 전환
|
|
*/
|
|
public function convertToProduction(): void
|
|
{
|
|
$this->update([
|
|
'tenant_type' => self::TYPE_STD,
|
|
'demo_expires_at' => null,
|
|
'demo_source_partner_id' => null,
|
|
]);
|
|
|
|
// options에서 데모 관련 키 제거
|
|
$options = $this->options ?? [];
|
|
unset(
|
|
$options[self::OPTION_DEMO_PRESET],
|
|
$options[self::OPTION_DEMO_LIMITS],
|
|
$options[self::OPTION_DEMO_READ_ONLY]
|
|
);
|
|
$this->update(['options' => $options ?: null]);
|
|
}
|
|
}
|