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']); } // ========================================================================= // 결제 명세서 // ========================================================================= /** * 결제 명세서 조회 */ public function statement(int $id): array { $tenantId = $this->tenantId(); // 테넌트 검증 및 결제 조회 $subscriptionIds = Subscription::query() ->where('tenant_id', $tenantId) ->pluck('id'); $payment = Payment::query() ->whereIn('subscription_id', $subscriptionIds) ->with(['subscription.plan']) ->findOrFail($id); // 테넌트 정보 조회 $tenant = Tenant::findOrFail($tenantId); $subscription = $payment->subscription; $plan = $subscription->plan; return [ 'statement_no' => sprintf('INV-%s-%06d', $payment->paid_at?->format('Ymd') ?? now()->format('Ymd'), $payment->id), 'issued_at' => now()->toIso8601String(), 'payment' => [ 'id' => $payment->id, 'amount' => $payment->amount, 'formatted_amount' => $payment->formatted_amount, 'payment_method' => $payment->payment_method, 'payment_method_label' => $payment->payment_method_label, 'transaction_id' => $payment->transaction_id, 'status' => $payment->status, 'status_label' => $payment->status_label, 'paid_at' => $payment->paid_at?->toIso8601String(), 'memo' => $payment->memo, ], 'subscription' => [ 'id' => $subscription->id, 'started_at' => $subscription->started_at?->toDateString(), 'ended_at' => $subscription->ended_at?->toDateString(), 'status' => $subscription->status, 'status_label' => $subscription->status_label, ], 'plan' => $plan ? [ 'id' => $plan->id, 'name' => $plan->name, 'code' => $plan->code, 'price' => $plan->price, 'billing_cycle' => $plan->billing_cycle, 'billing_cycle_label' => $plan->billing_cycle_label ?? $plan->billing_cycle, ] : null, 'customer' => [ 'tenant_id' => $tenant->id, 'company_name' => $tenant->company_name, 'business_number' => $tenant->business_number ?? null, 'representative' => $tenant->representative ?? null, 'address' => $tenant->address ?? null, 'email' => $tenant->email ?? null, 'phone' => $tenant->phone ?? null, ], 'items' => [ [ 'description' => $plan ? sprintf('%s 구독 (%s)', $plan->name, $subscription->started_at?->format('Y.m.d') ?? '-') : '구독 서비스', 'quantity' => 1, 'unit_price' => $payment->amount, 'amount' => $payment->amount, ], ], 'subtotal' => $payment->amount, 'tax' => 0, // VAT 별도 시 계산 필요 'total' => $payment->amount, ]; } }