Files
sam-api/app/Models/Tenants/Payment.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

289 lines
6.9 KiB
PHP

<?php
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',
'created_by',
'updated_by',
'deleted_by',
];
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();
}
}