tenantId(); $query = Subscription::query() ->where('tenant_id', $tenantId) ->with(['plan:id,name,code,price,billing_cycle']); // 상태 필터 if (! empty($params['status'])) { $query->where('status', $params['status']); } // 유효한 구독만 if (! empty($params['valid_only']) && $params['valid_only']) { $query->valid(); } // 만료 예정 (N일 이내) if (! empty($params['expiring_within'])) { $query->expiringWithin((int) $params['expiring_within']); } // 날짜 범위 필터 if (! empty($params['start_date'])) { $query->where('started_at', '>=', $params['start_date']); } if (! empty($params['end_date'])) { $query->where('started_at', '<=', $params['end_date']); } // 정렬 $sortBy = $params['sort_by'] ?? 'started_at'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 현재 활성 구독 */ public function current(): ?Subscription { $tenantId = $this->tenantId(); return Subscription::query() ->where('tenant_id', $tenantId) ->valid() ->with(['plan', 'payments' => function ($q) { $q->completed()->orderBy('paid_at', 'desc')->limit(5); }]) ->orderBy('started_at', 'desc') ->first(); } /** * 구독 상세 */ public function show(int $id): Subscription { $tenantId = $this->tenantId(); return Subscription::query() ->where('tenant_id', $tenantId) ->with([ 'plan', 'payments' => function ($q) { $q->orderBy('paid_at', 'desc'); }, ]) ->findOrFail($id); } // ========================================================================= // 구독 생성/취소 // ========================================================================= /** * 구독 생성 (결제 포함) */ public function store(array $data): Subscription { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); // 요금제 확인 $plan = Plan::active()->findOrFail($data['plan_id']); // 이미 활성 구독이 있는지 확인 $existingSubscription = Subscription::query() ->where('tenant_id', $tenantId) ->valid() ->first(); if ($existingSubscription) { throw new BadRequestHttpException(__('error.subscription.already_active')); } return DB::transaction(function () use ($data, $plan, $tenantId, $userId) { // 구독 생성 $subscription = Subscription::create([ 'tenant_id' => $tenantId, 'plan_id' => $plan->id, 'started_at' => $data['started_at'] ?? now(), 'status' => Subscription::STATUS_PENDING, 'created_by' => $userId, 'updated_by' => $userId, ]); // 결제 생성 (무료 요금제가 아닌 경우) if ($plan->price > 0) { $payment = Payment::create([ 'subscription_id' => $subscription->id, 'amount' => $plan->price, 'payment_method' => $data['payment_method'] ?? Payment::METHOD_CARD, 'status' => Payment::STATUS_PENDING, 'created_by' => $userId, 'updated_by' => $userId, ]); // 결제 완료 처리 (실제 PG 연동 시 수정 필요) if (! empty($data['auto_complete']) && $data['auto_complete']) { $payment->complete($data['transaction_id'] ?? null); // 구독 활성화 $subscription->activate(); } } else { // 무료 요금제는 바로 활성화 Payment::create([ 'subscription_id' => $subscription->id, 'amount' => 0, 'payment_method' => Payment::METHOD_FREE, 'status' => Payment::STATUS_COMPLETED, 'paid_at' => now(), 'created_by' => $userId, 'updated_by' => $userId, ]); $subscription->activate(); } return $subscription->fresh(['plan', 'payments']); }); } /** * 구독 취소 */ public function cancel(int $id, ?string $reason = null): Subscription { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $subscription = Subscription::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $subscription->isCancellable()) { throw new BadRequestHttpException(__('error.subscription.not_cancellable')); } $subscription->cancel($reason); $subscription->updated_by = $userId; $subscription->save(); return $subscription->fresh(['plan']); } /** * 구독 갱신 */ public function renew(int $id, array $data = []): Subscription { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $subscription = Subscription::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if ($subscription->status !== Subscription::STATUS_ACTIVE) { throw new BadRequestHttpException(__('error.subscription.not_renewable')); } return DB::transaction(function () use ($subscription, $data, $userId) { $plan = $subscription->plan; // 결제 생성 if ($plan->price > 0) { $payment = Payment::create([ 'subscription_id' => $subscription->id, 'amount' => $plan->price, 'payment_method' => $data['payment_method'] ?? Payment::METHOD_CARD, 'status' => Payment::STATUS_PENDING, 'created_by' => $userId, 'updated_by' => $userId, ]); // 결제 완료 처리 if (! empty($data['auto_complete']) && $data['auto_complete']) { $payment->complete($data['transaction_id'] ?? null); $subscription->renew(); } } else { // 무료 갱신 Payment::create([ 'subscription_id' => $subscription->id, 'amount' => 0, 'payment_method' => Payment::METHOD_FREE, 'status' => Payment::STATUS_COMPLETED, 'paid_at' => now(), 'created_by' => $userId, 'updated_by' => $userId, ]); $subscription->renew(); } $subscription->updated_by = $userId; $subscription->save(); return $subscription->fresh(['plan', 'payments']); }); } // ========================================================================= // 구독 상태 관리 // ========================================================================= /** * 구독 일시정지 */ public function suspend(int $id): Subscription { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $subscription = Subscription::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $subscription->suspend()) { throw new BadRequestHttpException(__('error.subscription.not_suspendable')); } $subscription->updated_by = $userId; $subscription->save(); return $subscription->fresh(['plan']); } /** * 구독 재개 */ public function resume(int $id): Subscription { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $subscription = Subscription::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $subscription->resume()) { throw new BadRequestHttpException(__('error.subscription.not_resumable')); } $subscription->updated_by = $userId; $subscription->save(); return $subscription->fresh(['plan']); } // ========================================================================= // 사용량 조회 // ========================================================================= /** * 사용량 조회 */ public function usage(): array { $tenantId = $this->tenantId(); $tenant = Tenant::with(['subscription.plan'])->findOrFail($tenantId); // 사용자 수 $userCount = $tenant->users()->count(); $maxUsers = $tenant->max_users ?? 0; // 저장공간 $storageUsed = $tenant->storage_used ?? 0; $storageLimit = $tenant->storage_limit ?? 0; // 구독 정보 $subscription = $tenant->subscription; $remainingDays = null; $planName = null; if ($subscription && $subscription->is_valid) { $remainingDays = $subscription->remaining_days; $planName = $subscription->plan?->name; } return [ 'users' => [ 'used' => $userCount, 'limit' => $maxUsers, 'percentage' => $maxUsers > 0 ? round(($userCount / $maxUsers) * 100, 1) : 0, ], 'storage' => [ 'used' => $storageUsed, 'used_formatted' => $tenant->getStorageUsedFormatted(), 'limit' => $storageLimit, 'limit_formatted' => $tenant->getStorageLimitFormatted(), 'percentage' => $storageLimit > 0 ? round(($storageUsed / $storageLimit) * 100, 1) : 0, ], 'subscription' => [ 'plan' => $planName, 'status' => $subscription?->status, 'remaining_days' => $remainingDays, 'started_at' => $subscription?->started_at?->toDateString(), 'ended_at' => $subscription?->ended_at?->toDateString(), ], ]; } // ========================================================================= // 데이터 내보내기 // ========================================================================= /** * 내보내기 요청 생성 */ public function createExport(array $data): DataExport { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); // 진행 중인 내보내기가 있는지 확인 $pendingExport = DataExport::where('tenant_id', $tenantId) ->whereIn('status', [DataExport::STATUS_PENDING, DataExport::STATUS_PROCESSING]) ->first(); if ($pendingExport) { throw new BadRequestHttpException(__('error.export.already_in_progress')); } $export = DataExport::create([ 'tenant_id' => $tenantId, 'export_type' => $data['export_type'] ?? DataExport::TYPE_ALL, 'status' => DataExport::STATUS_PENDING, 'options' => $data['options'] ?? null, 'created_by' => $userId, ]); // TODO: 비동기 Job 디스패치 // dispatch(new ProcessDataExport($export)); return $export; } /** * 내보내기 상태 조회 */ public function getExport(int $id): DataExport { $tenantId = $this->tenantId(); $export = DataExport::where('tenant_id', $tenantId)->find($id); if (! $export) { throw new NotFoundHttpException(__('error.export.not_found')); } return $export; } /** * 내보내기 목록 조회 */ public function getExports(array $params = []): LengthAwarePaginator { $tenantId = $this->tenantId(); $query = DataExport::where('tenant_id', $tenantId) ->with('creator:id,name,email'); // 상태 필터 if (! empty($params['status'])) { $query->where('status', $params['status']); } // 유형 필터 if (! empty($params['export_type'])) { $query->where('export_type', $params['export_type']); } $query->orderBy('created_at', 'desc'); $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } }