- Auditable 트레이트 신규 생성 (bootAuditable 패턴) - creating: created_by/updated_by 자동 채우기 - updating: updated_by 자동 채우기 - deleting: deleted_by 채우기 + saveQuietly() - created/updated/deleted: audit_logs 자동 기록 - 기존 AuditLogger 패턴과 동일한 try/catch 조용한 실패 - 변경된 필드만 before/after 기록 (updated 이벤트) - auditExclude 프로퍼티로 모델별 제외 필드 설정 가능 - 제외 대상: Attendance, StockTransaction, TodayIssue 등 고빈도/시스템 모델 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
290 lines
7.0 KiB
PHP
290 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace App\Models\Tenants;
|
|
|
|
use App\Traits\Auditable;
|
|
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 Auditable, 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();
|
|
}
|
|
}
|