tenantId(); // 테넌트의 구독에 속한 결제만 조회 $subscriptionIds = Subscription::query() ->where('tenant_id', $tenantId) ->pluck('id'); $query = Payment::query() ->whereIn('subscription_id', $subscriptionIds) ->with(['subscription.plan:id,name,code']); // 상태 필터 if (! empty($params['status'])) { $query->ofStatus($params['status']); } // 결제 수단 필터 if (! empty($params['payment_method'])) { $query->ofMethod($params['payment_method']); } // 날짜 범위 필터 $query->betweenDates( $params['start_date'] ?? null, $params['end_date'] ?? null ); // 검색 (거래 ID, 메모) if (! empty($params['search'])) { $search = $params['search']; $query->where(function ($q) use ($search) { $q->where('transaction_id', 'like', "%{$search}%") ->orWhere('memo', 'like', "%{$search}%"); }); } // 정렬 $sortBy = $params['sort_by'] ?? 'created_at'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 결제 상세 */ public function show(int $id): Payment { $tenantId = $this->tenantId(); // 테넌트 검증을 위해 구독 통해 조회 $subscriptionIds = Subscription::query() ->where('tenant_id', $tenantId) ->pluck('id'); return Payment::query() ->whereIn('subscription_id', $subscriptionIds) ->with(['subscription.plan']) ->findOrFail($id); } /** * 결제 요약 통계 */ public function summary(array $params = []): array { $tenantId = $this->tenantId(); $subscriptionIds = Subscription::query() ->where('tenant_id', $tenantId) ->pluck('id'); $query = Payment::query() ->whereIn('subscription_id', $subscriptionIds); // 날짜 범위 필터 if (! empty($params['start_date']) || ! empty($params['end_date'])) { $query->betweenDates( $params['start_date'] ?? null, $params['end_date'] ?? null ); } $stats = $query->selectRaw(' COUNT(*) as total_count, SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as completed_count, SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as pending_count, SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as failed_count, SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as cancelled_count, SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as refunded_count, SUM(CASE WHEN status = ? THEN amount ELSE 0 END) as total_completed_amount, SUM(CASE WHEN status = ? THEN amount ELSE 0 END) as total_refunded_amount ', [ Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_FAILED, Payment::STATUS_CANCELLED, Payment::STATUS_REFUNDED, Payment::STATUS_COMPLETED, Payment::STATUS_REFUNDED, ])->first(); // 결제 수단별 집계 $byMethod = Payment::query() ->whereIn('subscription_id', $subscriptionIds) ->completed() ->selectRaw('payment_method, COUNT(*) as count, SUM(amount) as total_amount') ->groupBy('payment_method') ->get() ->keyBy('payment_method') ->map(fn ($item) => [ 'count' => (int) $item->count, 'total_amount' => (float) $item->total_amount, ]) ->toArray(); return [ 'total_count' => (int) $stats->total_count, 'completed_count' => (int) $stats->completed_count, 'pending_count' => (int) $stats->pending_count, 'failed_count' => (int) $stats->failed_count, 'cancelled_count' => (int) $stats->cancelled_count, 'refunded_count' => (int) $stats->refunded_count, 'total_completed_amount' => (float) $stats->total_completed_amount, 'total_refunded_amount' => (float) $stats->total_refunded_amount, 'net_amount' => (float) ($stats->total_completed_amount - $stats->total_refunded_amount), 'by_method' => $byMethod, ]; } // ========================================================================= // 결제 처리 // ========================================================================= /** * 결제 생성 (수동) */ public function store(array $data): Payment { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); // 구독 확인 $subscription = Subscription::query() ->where('tenant_id', $tenantId) ->findOrFail($data['subscription_id']); return DB::transaction(function () use ($data, $subscription, $userId) { $payment = Payment::create([ 'subscription_id' => $subscription->id, 'amount' => $data['amount'], 'payment_method' => $data['payment_method'] ?? Payment::METHOD_CARD, 'transaction_id' => $data['transaction_id'] ?? null, 'status' => Payment::STATUS_PENDING, 'memo' => $data['memo'] ?? null, 'created_by' => $userId, 'updated_by' => $userId, ]); // 자동 완료 처리 if (! empty($data['auto_complete']) && $data['auto_complete']) { $payment->complete($data['transaction_id'] ?? null); } return $payment->fresh(['subscription.plan']); }); } /** * 결제 완료 처리 */ public function complete(int $id, ?string $transactionId = null): Payment { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $subscriptionIds = Subscription::query() ->where('tenant_id', $tenantId) ->pluck('id'); $payment = Payment::query() ->whereIn('subscription_id', $subscriptionIds) ->findOrFail($id); if (! $payment->complete($transactionId)) { throw new BadRequestHttpException(__('error.payment.not_completable')); } $payment->updated_by = $userId; $payment->save(); // 구독이 대기 중이면 활성화 $subscription = $payment->subscription; if ($subscription->status === Subscription::STATUS_PENDING) { $subscription->activate(); } return $payment->fresh(['subscription.plan']); } /** * 결제 취소 */ public function cancel(int $id, ?string $reason = null): Payment { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $subscriptionIds = Subscription::query() ->where('tenant_id', $tenantId) ->pluck('id'); $payment = Payment::query() ->whereIn('subscription_id', $subscriptionIds) ->findOrFail($id); if (! $payment->cancel($reason)) { throw new BadRequestHttpException(__('error.payment.not_cancellable')); } $payment->updated_by = $userId; $payment->save(); return $payment->fresh(['subscription.plan']); } /** * 환불 처리 */ public function refund(int $id, ?string $reason = null): Payment { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $subscriptionIds = Subscription::query() ->where('tenant_id', $tenantId) ->pluck('id'); $payment = Payment::query() ->whereIn('subscription_id', $subscriptionIds) ->findOrFail($id); if (! $payment->refund($reason)) { throw new BadRequestHttpException(__('error.payment.not_refundable')); } $payment->updated_by = $userId; $payment->save(); return $payment->fresh(['subscription.plan']); } }