Files
sam-api/app/Services/SubscriptionService.php
hskwon 45780ea351 feat: 구독/결제 API 확장 (Plan, Subscription, Payment)
- Plan/Subscription/Payment 모델에 상태 상수, 스코프, 헬퍼 메서드 추가
- PlanService, SubscriptionService, PaymentService 생성
- PlanController, SubscriptionController, PaymentController 생성
- FormRequest 9개 생성 (Plan 3개, Subscription 3개, Payment 3개)
- Swagger 문서 3개 생성 (PlanApi, SubscriptionApi, PaymentApi)
- API 라우트 22개 등록 (Plan 7개, Subscription 8개, Payment 7개)
- Pint 코드 스타일 정리
2025-12-18 16:20:29 +09:00

298 lines
9.2 KiB
PHP

<?php
namespace App\Services;
use App\Models\Tenants\Payment;
use App\Models\Tenants\Plan;
use App\Models\Tenants\Subscription;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class SubscriptionService extends Service
{
// =========================================================================
// 구독 목록/상세
// =========================================================================
/**
* 구독 목록
*/
public function index(array $params): LengthAwarePaginator
{
$tenantId = $this->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']);
}
}