feat: Phase 8 SaaS 확장 - 구독관리/결제내역 API 추가

- 사용량 조회 API (GET /subscriptions/usage)
- 데이터 내보내기 API (POST/GET /subscriptions/export)
- 결제 명세서 API (GET /payments/{id}/statement)
- DataExport 모델 및 마이그레이션 추가
This commit is contained in:
2025-12-19 16:53:49 +09:00
parent 0d49e4cc75
commit abaff1286e
13 changed files with 868 additions and 1 deletions

View File

@@ -0,0 +1,206 @@
<?php
namespace App\Models\Tenants;
use App\Models\Members\User;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* 데이터 내보내기 모델
*
* @property int $id
* @property int $tenant_id 테넌트 ID
* @property string $export_type 내보내기 유형
* @property string $status 상태
* @property string|null $file_path 파일 경로
* @property string|null $file_name 파일명
* @property int|null $file_size 파일 크기
* @property array|null $options 옵션
* @property \Carbon\Carbon|null $started_at 시작 시간
* @property \Carbon\Carbon|null $completed_at 완료 시간
* @property string|null $error_message 에러 메시지
* @property int|null $created_by 생성자
*/
class DataExport extends Model
{
use BelongsToTenant;
// =========================================================================
// 상수 정의
// =========================================================================
/** 내보내기 유형 */
public const TYPE_ALL = 'all';
public const TYPE_USERS = 'users';
public const TYPE_PRODUCTS = 'products';
public const TYPE_ORDERS = 'orders';
public const TYPE_CLIENTS = 'clients';
public const TYPES = [
self::TYPE_ALL,
self::TYPE_USERS,
self::TYPE_PRODUCTS,
self::TYPE_ORDERS,
self::TYPE_CLIENTS,
];
/** 상태 */
public const STATUS_PENDING = 'pending';
public const STATUS_PROCESSING = 'processing';
public const STATUS_COMPLETED = 'completed';
public const STATUS_FAILED = 'failed';
public const STATUSES = [
self::STATUS_PENDING,
self::STATUS_PROCESSING,
self::STATUS_COMPLETED,
self::STATUS_FAILED,
];
/** 상태 라벨 */
public const STATUS_LABELS = [
self::STATUS_PENDING => '대기중',
self::STATUS_PROCESSING => '처리중',
self::STATUS_COMPLETED => '완료',
self::STATUS_FAILED => '실패',
];
// =========================================================================
// 모델 설정
// =========================================================================
protected $fillable = [
'tenant_id',
'export_type',
'status',
'file_path',
'file_name',
'file_size',
'options',
'started_at',
'completed_at',
'error_message',
'created_by',
];
protected $casts = [
'options' => 'array',
'file_size' => 'integer',
'started_at' => 'datetime',
'completed_at' => 'datetime',
];
protected $attributes = [
'status' => self::STATUS_PENDING,
];
// =========================================================================
// 관계
// =========================================================================
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
// =========================================================================
// 접근자
// =========================================================================
/**
* 상태 라벨
*/
public function getStatusLabelAttribute(): string
{
return self::STATUS_LABELS[$this->status] ?? $this->status;
}
/**
* 파일 크기 포맷
*/
public function getFileSizeFormattedAttribute(): string
{
if (! $this->file_size) {
return '-';
}
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($this->file_size, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, 2).' '.$units[$pow];
}
/**
* 완료 여부
*/
public function getIsCompletedAttribute(): bool
{
return $this->status === self::STATUS_COMPLETED;
}
/**
* 다운로드 가능 여부
*/
public function getIsDownloadableAttribute(): bool
{
return $this->status === self::STATUS_COMPLETED && $this->file_path;
}
// =========================================================================
// 헬퍼 메서드
// =========================================================================
/**
* 처리 시작
*/
public function markAsProcessing(): bool
{
$this->status = self::STATUS_PROCESSING;
$this->started_at = now();
return $this->save();
}
/**
* 처리 완료
*/
public function markAsCompleted(string $filePath, string $fileName, int $fileSize): bool
{
$this->status = self::STATUS_COMPLETED;
$this->file_path = $filePath;
$this->file_name = $fileName;
$this->file_size = $fileSize;
$this->completed_at = now();
return $this->save();
}
/**
* 처리 실패
*/
public function markAsFailed(string $errorMessage): bool
{
$this->status = self::STATUS_FAILED;
$this->error_message = $errorMessage;
$this->completed_at = now();
return $this->save();
}
}