Files
sam-docs/features/settlement/subscriptions.md

261 lines
7.5 KiB
Markdown
Raw Normal View History

# 구독료정산
## 개요
구독료정산은 고객사별 구독 플랜과 과금을 관리하는 기능입니다.
플랜별 월정액 관리, 과금주기(월/연), MRR/ARR 집계, 사용자 수 추적을 지원합니다.
- **라우트**: `GET /finance/subscription`
- **UI 기술**: React 18 + Babel (브라우저 트랜스파일링)
## 파일 구조
```
mng/
├── app/Http/Controllers/Finance/
│ └── SubscriptionController.php # MNG 컨트롤러 (4개 메서드)
├── app/Models/Finance/
│ └── Subscription.php # MNG 구독 모델
└── resources/views/finance/
└── subscription.blade.php # React 기반 단일 페이지
api/
├── app/Http/Controllers/Api/V1/
│ └── SubscriptionController.php # API 컨트롤러 (REST)
├── app/Services/
│ └── SubscriptionService.php # 비즈니스 로직 서비스
├── app/Models/Tenants/
│ └── Subscription.php # API 구독 모델 (상세 구현)
└── database/migrations/
├── 2026_02_04_230008_create_subscriptions_table.php
└── 2025_12_22_222722_add_cancel_columns_to_subscriptions_table.php
```
## 라우트
### MNG 라우트
```php
// routes/web.php (finance prefix 그룹 내)
GET /subscription → Blade 페이지 렌더링 (HX-Redirect)
// API 라우트 (subscriptions prefix)
GET /subscriptions/list → index() 목록 + 통계 (JSON)
POST /subscriptions/store → store() 등록
PUT /subscriptions/{id} → update() 수정
DELETE /subscriptions/{id} → destroy() 삭제
```
### API 라우트
```php
// routes/api.php (v1/subscriptions prefix)
GET / → index() 목록 (페이지네이션)
GET /{id} → show() 상세 조회
POST / → store() 생성
PUT /{id} → update() 수정
DELETE /{id} → destroy() 삭제
POST /{id}/cancel → cancel() 구독 취소
POST /{id}/suspend → suspend() 일시정지
POST /{id}/resume → resume() 재개
```
## 컨트롤러
### SubscriptionController (MNG)
| 메서드 | HTTP | 설명 |
|--------|------|------|
| `index()` | GET | 목록 + MRR/ARR 통계 (검색, 상태 필터) |
| `store()` | POST | 구독 등록 |
| `update()` | PUT | 구독 수정 |
| `destroy()` | DELETE | 삭제 (Soft Delete) |
### index() 응답 구조
```json
{
"success": true,
"data": [
{
"id": 1,
"customer": "A사",
"plan": "Business",
"monthlyFee": 500000,
"billingCycle": "monthly",
"startDate": "2026-01-01",
"nextBilling": "2026-03-01",
"status": "active",
"users": 10,
"memo": ""
}
],
"stats": {
"activeCount": 15,
"monthlyRecurring": 7500000,
"yearlyRecurring": 90000000,
"totalUsers": 150
}
}
```
## 핵심 로직
### MRR/ARR 계산
```
MRR (Monthly Recurring Revenue)
= SUM(active 구독의 monthly_fee) (billingCycle=monthly)
+ SUM(active 구독의 monthly_fee / 12) (billingCycle=yearly를 월 환산)
ARR (Annual Recurring Revenue)
= MRR × 12
```
### 구독 상태 관리 (API 모델)
```
pending (대기)
↓ activate()
active (활성)
├── cancel(reason) → cancelled (취소)
├── suspend() → suspended (일시정지)
│ ↓ resume() → active (재활성)
└── (만료) → expired
```
### 구독 플랜
| 플랜 | 설명 |
|------|------|
| Starter | 소규모 기본 플랜 |
| Business | 중규모 비즈니스 플랜 |
| Enterprise | 대규모 기업 플랜 |
### 과금 주기
| 주기 | 설명 |
|------|------|
| monthly | 월간 과금 |
| yearly | 연간 과금 |
## 모델
### Subscription (MNG)
**테이블**: `subscriptions`
| 필드 | 타입 | 설명 |
|------|------|------|
| `tenant_id` | bigint | 테넌트 ID |
| `customer` | string | 고객사명 |
| `plan` | string | 플랜명 (Starter/Business/Enterprise) |
| `monthly_fee` | int | 월 정액 |
| `billing_cycle` | string | monthly / yearly |
| `start_date` | date | 시작일 |
| `next_billing` | date | 다음 과금일 |
| `status` | string | active / trial / cancelled |
| `users` | int | 사용자 수 |
| `memo` | text | 메모 |
| `cancelled_at` | timestamp | 취소 일시 |
| `cancel_reason` | string(500) | 취소 사유 |
- SoftDeletes 적용
- Casts: monthly_fee, users → integer, start_date, next_billing → date
- Scope: `forTenant($tenantId)`
### Subscription (API - 상세 구현)
API 프로젝트의 모델은 더 상세한 구독 라이프사이클을 지원합니다:
#### 상태 상수
```php
const STATUS_ACTIVE = 'active';
const STATUS_CANCELLED = 'cancelled';
const STATUS_EXPIRED = 'expired';
const STATUS_PENDING = 'pending';
const STATUS_SUSPENDED = 'suspended';
```
#### Relationships
```php
$subscription->tenant // BelongsTo Tenant
$subscription->plan // BelongsTo Plan
$subscription->payments // HasMany Payment
```
#### 주요 Accessor
```php
$subscription->statusLabel // 상태 한글 라벨
$subscription->isExpired // 만료 여부
$subscription->remainingDays // 잔여 일수
$subscription->isValid // 유효 여부 (active + 미만료)
$subscription->totalPaid // 총 결제 금액
```
#### 주요 메서드
```php
$subscription->activate() // 활성화
$subscription->renew($newEndDate) // 갱신
$subscription->cancel($reason) // 취소
$subscription->suspend() // 일시정지
$subscription->resume() // 재개
$subscription->isCancellable() // 취소 가능 여부
```
## 서비스 클래스 (API)
### SubscriptionService
| 메서드 | 설명 |
|--------|------|
| `index(params)` | 페이지네이션 목록 (필터, 정렬) |
| `current()` | 현재 활성 구독 조회 |
| `show(id)` | 상세 (플랜, 최근 결제 포함) |
| `store(data)` | 구독 생성 (결제 처리) |
| `update(id, data)` | 구독 수정 |
| `cancel(id, reason)` | 구독 취소 |
| `suspend(id)` | 일시정지 |
| `resume(id)` | 재개 |
| `renew(id, newEndDate)` | 갱신 |
## 뷰 구성 (React)
### subscription.blade.php
```
┌─ 페이지 헤더 ──────────────────────
│ 제목: "구독료정산"
│ [CSV 내보내기] [등록] 버튼
├─ 통계 카드 (4열) ──────────────────
│ 활성 구독 | MRR (월간 반복 수익) | ARR (연간 반복 수익) | 총 사용자
├─ 필터 영역 ────────────────────────
│ 검색 (고객사명) | 플랜 (Starter/Business/Enterprise) | 상태 (active/trial/cancelled)
├─ 구독 목록 테이블 ─────────────────
│ 고객사 | 플랜 | 월정액 | 과금주기 | 다음과금일 | 사용자 | 상태 | 작업
│ └─ 플랜: Starter(회색), Business(파랑), Enterprise(보라) 배지
│ └─ 상태: active(초록), trial(파랑), cancelled(빨강) 배지
│ └─ 작업: 수정/삭제 버튼
├─ 등록/수정 모달 ───────────────────
│ 고객사명, 플랜 선택
│ 월정액, 과금주기(월/연)
│ 시작일, 다음 과금일
│ 상태, 사용자 수, 메모
│ [삭제] [취소] [등록/저장]
└─ 비어있을 때: 안내 메시지
```
## HTMX 호환성
- React 기반 페이지이므로 **HX-Redirect 필요**
- `@push('scripts')` 블록에 React/Babel 스크립트 포함