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 코드 스타일 정리
This commit is contained in:
@@ -2,26 +2,287 @@
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 결제 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $subscription_id 구독 ID
|
||||
* @property float $amount 결제 금액
|
||||
* @property string $payment_method 결제 수단
|
||||
* @property string|null $transaction_id PG 거래 ID
|
||||
* @property Carbon|null $paid_at 결제일시
|
||||
* @property string $status 결제 상태
|
||||
* @property string|null $memo 메모
|
||||
*
|
||||
* @mixin IdeHelperPayment
|
||||
*/
|
||||
class Payment extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
// =========================================================================
|
||||
// 상수 정의
|
||||
// =========================================================================
|
||||
|
||||
/** 결제 상태 */
|
||||
public const STATUS_PENDING = 'pending';
|
||||
|
||||
public const STATUS_COMPLETED = 'completed';
|
||||
|
||||
public const STATUS_FAILED = 'failed';
|
||||
|
||||
public const STATUS_CANCELLED = 'cancelled';
|
||||
|
||||
public const STATUS_REFUNDED = 'refunded';
|
||||
|
||||
public const STATUSES = [
|
||||
self::STATUS_PENDING,
|
||||
self::STATUS_COMPLETED,
|
||||
self::STATUS_FAILED,
|
||||
self::STATUS_CANCELLED,
|
||||
self::STATUS_REFUNDED,
|
||||
];
|
||||
|
||||
/** 상태 라벨 */
|
||||
public const STATUS_LABELS = [
|
||||
self::STATUS_PENDING => '대기',
|
||||
self::STATUS_COMPLETED => '완료',
|
||||
self::STATUS_FAILED => '실패',
|
||||
self::STATUS_CANCELLED => '취소',
|
||||
self::STATUS_REFUNDED => '환불',
|
||||
];
|
||||
|
||||
/** 결제 수단 */
|
||||
public const METHOD_CARD = 'card';
|
||||
|
||||
public const METHOD_BANK = 'bank';
|
||||
|
||||
public const METHOD_VIRTUAL = 'virtual';
|
||||
|
||||
public const METHOD_CASH = 'cash';
|
||||
|
||||
public const METHOD_FREE = 'free';
|
||||
|
||||
public const PAYMENT_METHODS = [
|
||||
self::METHOD_CARD,
|
||||
self::METHOD_BANK,
|
||||
self::METHOD_VIRTUAL,
|
||||
self::METHOD_CASH,
|
||||
self::METHOD_FREE,
|
||||
];
|
||||
|
||||
/** 결제 수단 라벨 */
|
||||
public const METHOD_LABELS = [
|
||||
self::METHOD_CARD => '카드',
|
||||
self::METHOD_BANK => '계좌이체',
|
||||
self::METHOD_VIRTUAL => '가상계좌',
|
||||
self::METHOD_CASH => '현금',
|
||||
self::METHOD_FREE => '무료',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 모델 설정
|
||||
// =========================================================================
|
||||
|
||||
protected $fillable = [
|
||||
'subscription_id', 'amount', 'payment_method', 'transaction_id', 'paid_at', 'status', 'memo',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'subscription_id',
|
||||
'amount',
|
||||
'payment_method',
|
||||
'transaction_id',
|
||||
'paid_at',
|
||||
'status',
|
||||
'memo',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
public function subscription()
|
||||
protected $casts = [
|
||||
'amount' => 'float',
|
||||
'paid_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'status' => self::STATUS_PENDING,
|
||||
'payment_method' => self::METHOD_CARD,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 완료된 결제만
|
||||
*/
|
||||
public function scopeCompleted(Builder $query): Builder
|
||||
{
|
||||
return $query->where('status', self::STATUS_COMPLETED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 상태
|
||||
*/
|
||||
public function scopeOfStatus(Builder $query, string $status): Builder
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 결제 수단
|
||||
*/
|
||||
public function scopeOfMethod(Builder $query, string $method): Builder
|
||||
{
|
||||
return $query->where('payment_method', $method);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기간별 필터
|
||||
*/
|
||||
public function scopeBetweenDates(Builder $query, ?string $startDate, ?string $endDate): Builder
|
||||
{
|
||||
if ($startDate) {
|
||||
$query->where('paid_at', '>=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$query->where('paid_at', '<=', $endDate.' 23:59:59');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 관계
|
||||
// =========================================================================
|
||||
|
||||
public function subscription(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Subscription::class);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 접근자
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 상태 라벨
|
||||
*/
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
return self::STATUS_LABELS[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 결제 수단 라벨
|
||||
*/
|
||||
public function getPaymentMethodLabelAttribute(): string
|
||||
{
|
||||
return self::METHOD_LABELS[$this->payment_method] ?? $this->payment_method;
|
||||
}
|
||||
|
||||
/**
|
||||
* 포맷된 금액
|
||||
*/
|
||||
public function getFormattedAmountAttribute(): string
|
||||
{
|
||||
return number_format($this->amount).'원';
|
||||
}
|
||||
|
||||
/**
|
||||
* 결제 완료 여부
|
||||
*/
|
||||
public function getIsCompletedAttribute(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_COMPLETED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 환불 가능 여부
|
||||
*/
|
||||
public function getIsRefundableAttribute(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_COMPLETED;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 결제 완료 처리
|
||||
*/
|
||||
public function complete(?string $transactionId = null): bool
|
||||
{
|
||||
if ($this->status !== self::STATUS_PENDING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->status = self::STATUS_COMPLETED;
|
||||
$this->paid_at = now();
|
||||
|
||||
if ($transactionId) {
|
||||
$this->transaction_id = $transactionId;
|
||||
}
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 결제 실패 처리
|
||||
*/
|
||||
public function fail(?string $reason = null): bool
|
||||
{
|
||||
if ($this->status !== self::STATUS_PENDING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->status = self::STATUS_FAILED;
|
||||
|
||||
if ($reason) {
|
||||
$this->memo = $reason;
|
||||
}
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 결제 취소 처리
|
||||
*/
|
||||
public function cancel(?string $reason = null): bool
|
||||
{
|
||||
if (! in_array($this->status, [self::STATUS_PENDING, self::STATUS_COMPLETED])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->status = self::STATUS_CANCELLED;
|
||||
|
||||
if ($reason) {
|
||||
$this->memo = $reason;
|
||||
}
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 환불 처리
|
||||
*/
|
||||
public function refund(?string $reason = null): bool
|
||||
{
|
||||
if ($this->status !== self::STATUS_COMPLETED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->status = self::STATUS_REFUNDED;
|
||||
|
||||
if ($reason) {
|
||||
$this->memo = $reason;
|
||||
}
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,68 @@
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 요금제 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name 요금제명
|
||||
* @property string $code 요금제 코드
|
||||
* @property string|null $description 설명
|
||||
* @property float $price 가격
|
||||
* @property string $billing_cycle 결제 주기
|
||||
* @property array|null $features 기능 목록
|
||||
* @property bool $is_active 활성 여부
|
||||
*
|
||||
* @mixin IdeHelperPlan
|
||||
*/
|
||||
class Plan extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
// =========================================================================
|
||||
// 상수 정의
|
||||
// =========================================================================
|
||||
|
||||
/** 결제 주기 */
|
||||
public const BILLING_MONTHLY = 'monthly';
|
||||
|
||||
public const BILLING_YEARLY = 'yearly';
|
||||
|
||||
public const BILLING_LIFETIME = 'lifetime';
|
||||
|
||||
public const BILLING_CYCLES = [
|
||||
self::BILLING_MONTHLY,
|
||||
self::BILLING_YEARLY,
|
||||
self::BILLING_LIFETIME,
|
||||
];
|
||||
|
||||
/** 결제 주기 라벨 */
|
||||
public const BILLING_CYCLE_LABELS = [
|
||||
self::BILLING_MONTHLY => '월간',
|
||||
self::BILLING_YEARLY => '연간',
|
||||
self::BILLING_LIFETIME => '평생',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 모델 설정
|
||||
// =========================================================================
|
||||
|
||||
protected $fillable = [
|
||||
'name', 'code', 'description', 'price', 'billing_cycle', 'features', 'is_active',
|
||||
'name',
|
||||
'code',
|
||||
'description',
|
||||
'price',
|
||||
'billing_cycle',
|
||||
'features',
|
||||
'is_active',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -22,8 +72,103 @@ class Plan extends Model
|
||||
'price' => 'float',
|
||||
];
|
||||
|
||||
public function subscriptions()
|
||||
protected $attributes = [
|
||||
'is_active' => true,
|
||||
'billing_cycle' => self::BILLING_MONTHLY,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 활성 요금제만
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결제 주기별 필터
|
||||
*/
|
||||
public function scopeOfCycle(Builder $query, string $cycle): Builder
|
||||
{
|
||||
return $query->where('billing_cycle', $cycle);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 관계
|
||||
// =========================================================================
|
||||
|
||||
public function subscriptions(): HasMany
|
||||
{
|
||||
return $this->hasMany(Subscription::class);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 접근자
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 결제 주기 라벨
|
||||
*/
|
||||
public function getBillingCycleLabelAttribute(): string
|
||||
{
|
||||
return self::BILLING_CYCLE_LABELS[$this->billing_cycle] ?? $this->billing_cycle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 포맷된 가격
|
||||
*/
|
||||
public function getFormattedPriceAttribute(): string
|
||||
{
|
||||
return number_format($this->price).'원';
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 구독 수
|
||||
*/
|
||||
public function getActiveSubscriptionCountAttribute(): int
|
||||
{
|
||||
return $this->subscriptions()
|
||||
->where('status', Subscription::STATUS_ACTIVE)
|
||||
->count();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 월 환산 가격 계산
|
||||
*/
|
||||
public function getMonthlyPrice(): float
|
||||
{
|
||||
return match ($this->billing_cycle) {
|
||||
self::BILLING_YEARLY => round($this->price / 12, 2),
|
||||
self::BILLING_LIFETIME => 0,
|
||||
default => $this->price,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 연 환산 가격 계산
|
||||
*/
|
||||
public function getYearlyPrice(): float
|
||||
{
|
||||
return match ($this->billing_cycle) {
|
||||
self::BILLING_MONTHLY => $this->price * 12,
|
||||
self::BILLING_LIFETIME => 0,
|
||||
default => $this->price,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 기능 포함 여부
|
||||
*/
|
||||
public function hasFeature(string $feature): bool
|
||||
{
|
||||
return in_array($feature, $this->features ?? [], true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,36 +2,312 @@
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 구독 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id 테넌트 ID
|
||||
* @property int $plan_id 요금제 ID
|
||||
* @property Carbon $started_at 시작일
|
||||
* @property Carbon|null $ended_at 종료일
|
||||
* @property string $status 상태
|
||||
* @property Carbon|null $cancelled_at 취소일
|
||||
* @property string|null $cancel_reason 취소 사유
|
||||
*
|
||||
* @mixin IdeHelperSubscription
|
||||
*/
|
||||
class Subscription extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
// =========================================================================
|
||||
// 상수 정의
|
||||
// =========================================================================
|
||||
|
||||
/** 구독 상태 */
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
|
||||
public const STATUS_CANCELLED = 'cancelled';
|
||||
|
||||
public const STATUS_EXPIRED = 'expired';
|
||||
|
||||
public const STATUS_PENDING = 'pending';
|
||||
|
||||
public const STATUS_SUSPENDED = 'suspended';
|
||||
|
||||
public const STATUSES = [
|
||||
self::STATUS_ACTIVE,
|
||||
self::STATUS_CANCELLED,
|
||||
self::STATUS_EXPIRED,
|
||||
self::STATUS_PENDING,
|
||||
self::STATUS_SUSPENDED,
|
||||
];
|
||||
|
||||
/** 상태 라벨 */
|
||||
public const STATUS_LABELS = [
|
||||
self::STATUS_ACTIVE => '활성',
|
||||
self::STATUS_CANCELLED => '취소됨',
|
||||
self::STATUS_EXPIRED => '만료됨',
|
||||
self::STATUS_PENDING => '대기',
|
||||
self::STATUS_SUSPENDED => '일시정지',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 모델 설정
|
||||
// =========================================================================
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id', 'plan_id', 'started_at', 'ended_at', 'status',
|
||||
'tenant_id',
|
||||
'plan_id',
|
||||
'started_at',
|
||||
'ended_at',
|
||||
'status',
|
||||
'cancelled_at',
|
||||
'cancel_reason',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'started_at', 'ended_at',
|
||||
protected $casts = [
|
||||
'started_at' => 'datetime',
|
||||
'ended_at' => 'datetime',
|
||||
'cancelled_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function tenant()
|
||||
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()
|
||||
public function plan(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Plan::class);
|
||||
}
|
||||
|
||||
public function payments()
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user