'활성', self::STATUS_CANCELLED => '취소됨', self::STATUS_EXPIRED => '만료됨', self::STATUS_PENDING => '대기', self::STATUS_SUSPENDED => '일시정지', ]; // ========================================================================= // 모델 설정 // ========================================================================= protected $fillable = [ 'tenant_id', 'plan_id', 'started_at', 'ended_at', 'status', 'cancelled_at', 'cancel_reason', 'created_by', 'updated_by', 'deleted_by', ]; protected $casts = [ 'started_at' => 'datetime', 'ended_at' => 'datetime', 'cancelled_at' => 'datetime', ]; protected $attributes = [ 'status' => self::STATUS_PENDING, ]; // ========================================================================= // 스코프 // ========================================================================= /** * 활성 구독만 */ public function scopeActive(Builder $query): Builder { return $query->where('status', self::STATUS_ACTIVE); } /** * 유효한 구독 (활성 + 미만료) */ public function scopeValid(Builder $query): Builder { return $query->where('status', self::STATUS_ACTIVE) ->where(function ($q) { $q->whereNull('ended_at') ->orWhere('ended_at', '>', now()); }); } /** * 만료 예정 (N일 이내) */ public function scopeExpiringWithin(Builder $query, int $days): Builder { return $query->where('status', self::STATUS_ACTIVE) ->whereNotNull('ended_at') ->where('ended_at', '<=', now()->addDays($days)) ->where('ended_at', '>', now()); } /** * 특정 테넌트 */ public function scopeForTenant(Builder $query, int $tenantId): Builder { return $query->where('tenant_id', $tenantId); } // ========================================================================= // 관계 // ========================================================================= public function tenant(): BelongsTo { return $this->belongsTo(Tenant::class); } public function plan(): BelongsTo { return $this->belongsTo(Plan::class); } public function payments(): HasMany { return $this->hasMany(Payment::class); } // ========================================================================= // 접근자 // ========================================================================= /** * 상태 라벨 */ public function getStatusLabelAttribute(): string { return self::STATUS_LABELS[$this->status] ?? $this->status; } /** * 만료 여부 */ public function getIsExpiredAttribute(): bool { if (! $this->ended_at) { return false; } return $this->ended_at->isPast(); } /** * 남은 일수 */ public function getRemainingDaysAttribute(): ?int { if (! $this->ended_at) { return null; // 무제한 } if ($this->is_expired) { return 0; } return now()->diffInDays($this->ended_at, false); } /** * 유효 여부 */ public function getIsValidAttribute(): bool { return $this->status === self::STATUS_ACTIVE && ! $this->is_expired; } /** * 총 결제 금액 */ public function getTotalPaidAttribute(): float { return (float) $this->payments() ->where('status', Payment::STATUS_COMPLETED) ->sum('amount'); } // ========================================================================= // 헬퍼 메서드 // ========================================================================= /** * 구독 활성화 */ public function activate(): bool { if ($this->status !== self::STATUS_PENDING) { return false; } $this->status = self::STATUS_ACTIVE; $this->started_at = $this->started_at ?? now(); // 종료일 계산 (요금제 주기에 따라) if (! $this->ended_at && $this->plan) { $this->ended_at = match ($this->plan->billing_cycle) { Plan::BILLING_MONTHLY => $this->started_at->copy()->addMonth(), Plan::BILLING_YEARLY => $this->started_at->copy()->addYear(), Plan::BILLING_LIFETIME => null, default => $this->started_at->copy()->addMonth(), }; } return $this->save(); } /** * 구독 갱신 */ public function renew(?Carbon $newEndDate = null): bool { if ($this->status !== self::STATUS_ACTIVE) { return false; } if ($newEndDate) { $this->ended_at = $newEndDate; } elseif ($this->plan) { $baseDate = $this->ended_at ?? now(); $this->ended_at = match ($this->plan->billing_cycle) { Plan::BILLING_MONTHLY => $baseDate->copy()->addMonth(), Plan::BILLING_YEARLY => $baseDate->copy()->addYear(), Plan::BILLING_LIFETIME => null, default => $baseDate->copy()->addMonth(), }; } return $this->save(); } /** * 구독 취소 */ public function cancel(?string $reason = null): bool { if (! in_array($this->status, [self::STATUS_ACTIVE, self::STATUS_PENDING])) { return false; } $this->status = self::STATUS_CANCELLED; $this->cancelled_at = now(); $this->cancel_reason = $reason; return $this->save(); } /** * 구독 일시정지 */ public function suspend(): bool { if ($this->status !== self::STATUS_ACTIVE) { return false; } $this->status = self::STATUS_SUSPENDED; return $this->save(); } /** * 구독 재개 */ public function resume(): bool { if ($this->status !== self::STATUS_SUSPENDED) { return false; } $this->status = self::STATUS_ACTIVE; return $this->save(); } /** * 취소 가능 여부 */ public function isCancellable(): bool { return in_array($this->status, [self::STATUS_ACTIVE, self::STATUS_PENDING]); } }