# 機能設計書 98-サブスクリプション管理

## 概要

本ドキュメントは、Giselleプラットフォームにおけるstripeを使用したサブスクリプション管理機能について記述する。

### 本機能の処理概要

**業務上の目的・背景**：チームの有料プラン（Pro）への契約と解約を管理するため、Stripe v2 Billing APIを使用したサブスクリプション管理機能を提供する。Webhookイベントを通じて、サブスクリプションの有効化・キャンセルを検知し、チームのプラン状態を自動更新する。

**機能の利用シーン**：
- Proプランへのアップグレード（チェックアウト完了時）
- 新規チーム作成とProプラン契約の同時実行
- サブスクリプションのキャンセル（Freeプランへのダウングレード）

**主要な処理内容**：
1. Stripe v2 Webhook受信と署名検証
2. servicing_activatedイベント処理（Proプラン有効化）
3. servicing_canceledイベント処理（Freeプランへダウングレード）
4. 新規チーム作成（メタデータからチーム名・ユーザー情報を取得）
5. 既存チームのアップグレード（メタデータからチームDBIdを取得）
6. サブスクリプション履歴の記録
7. 冪等性保証（重複Webhookの安全な処理）

**関連システム・外部連携**：
- Stripe v2 Billing API
- Stripe Webhooks

**権限による制御**：Webhookは署名検証で認証。ユーザーアクションは不要。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 26 | チーム設定画面 | 補助機能 | プラン情報の表示 |

## 機能種別

データ連携 / Webhook処理 / サブスクリプション管理

## 入力仕様

### 入力パラメータ（Webhook）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| body | string | Yes | Webhookペイロード | JSON形式 |
| stripe-signature | string | Yes | 署名ヘッダー | Stripe署名検証 |

### 入力イベント

| イベント | 説明 |
|---------|------|
| v2.billing.pricing_plan_subscription.servicing_activated | サブスクリプション有効化 |
| v2.billing.pricing_plan_subscription.servicing_canceled | サブスクリプションキャンセル |

### 入力データソース

- Stripe Webhooks
- Stripe v2 Billing API

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| Response | Response | HTTP応答（200 OK or 400 Error） |

### 出力先

- データベース（teams, teamMemberships, stripePricingPlanSubscriptionHistories）

## 処理フロー

### 処理シーケンス

```
1. Webhook受信
   └─ ボディ取得 → 署名取得 → 署名検証 → イベント構築

2. イベント種別判定
   └─ servicing_activated → handlePricingPlanServicingActivated
   └─ servicing_canceled → handlePricingPlanServicingCanceled

3. servicing_activated処理
   └─ サブスクリプション取得 → ケイデンス取得 → メタデータ解析 → チーム処理分岐
   └─ 既存サブスクリプション確認（冪等性） → 新規/アップグレード/更新

4. servicing_canceled処理
   └─ サブスクリプション取得 → 履歴検索 → チームダウングレード → 履歴記録
```

### フローチャート

```mermaid
flowchart TD
    A[Webhook受信] --> B[署名検証]
    B --> C{検証成功?}
    C -->|No| D[400エラー]
    C -->|Yes| E{イベント種別}

    E -->|servicing_activated| F[サブスクリプション取得]
    F --> G[ケイデンス取得]
    G --> H[メタデータ解析]
    H --> I{既存履歴あり?}
    I -->|Yes| J[updateExistingTeam]
    I -->|No| K{メタデータ種別}
    K -->|UPGRADING_TEAM_DB_ID| L[upgradeExistingTeam]
    K -->|DRAFT_TEAM| M[createNewProTeam]

    E -->|servicing_canceled| N[サブスクリプション取得]
    N --> O[履歴検索]
    O --> P{履歴あり?}
    P -->|No| Q[エラー]
    P -->|Yes| R[チームダウングレード]
    R --> S[履歴記録]

    J --> T[200 OK]
    L --> T
    M --> T
    S --> T
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | 冪等性保証 | 同一サブスクリプションの重複処理を安全に処理 | servicing_activated時 |
| BR-002 | メタデータ分岐 | UPGRADING_TEAM_DB_IDで既存アップグレード、DRAFT_TEAM_*で新規作成 | servicing_activated時 |
| BR-003 | プラン保護 | internal/enterpriseプランはアップグレード・ダウングレード対象外 | 常時 |
| BR-004 | 履歴記録 | すべてのサブスクリプション状態変更を履歴に記録 | 常時 |
| BR-005 | 顧客ID管理 | ケイデンスのpayerからcustomer IDを取得 | サブスクリプション処理時 |

### 計算ロジック

特になし

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 新規Proチーム作成 | teams | INSERT | Proプランチーム作成 |
| チームメンバー追加 | teamMemberships | INSERT | adminロールで追加 |
| 既存チームアップグレード | teams | UPDATE | plan, activeSubscriptionId, activeCustomerId更新 |
| チームダウングレード | teams | UPDATE | plan=free, activeSubscriptionId=null |
| 履歴記録 | stripePricingPlanSubscriptionHistories | INSERT | サブスクリプション状態記録 |

### テーブル別操作詳細

#### teams

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id, name, plan, activeSubscriptionId, activeCustomerId | メタデータから | createNewProTeam |
| UPDATE | plan, activeSubscriptionId, activeCustomerId | "pro", サブスクリプションID, 顧客ID | upgradeExistingTeam |
| UPDATE | plan, activeSubscriptionId, activeCustomerId | "free", null, null | ダウングレード |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| WEBHOOK_SECRET_NOT_CONFIGURED | 設定エラー | 環境変数未設定 | 環境変数設定 |
| MISSING_SIGNATURE | 認証エラー | 署名ヘッダー不在 | 正しいリクエスト送信 |
| SIGNATURE_VERIFICATION_FAILED | 認証エラー | 署名検証失敗 | Webhook設定確認 |
| MISSING_RELATED_OBJECT | データ不正 | サブスクリプションIDなし | Stripeサポートに問い合わせ |
| INVALID_METADATA | データ不正 | 必要なメタデータ不足 | チェックアウト設定確認 |
| TEAM_NOT_FOUND | データ不在 | チームが存在しない | データ整合性確認 |
| NO_SUBSCRIPTION_HISTORY | データ不在 | キャンセル時に履歴なし | 履歴データ確認 |

### リトライ仕様

- Stripe Webhookの自動リトライ機能に依存
- 冪等性により安全にリトライ可能

## トランザクション仕様

- すべてのDB操作はdb.transaction内で実行
- アクティブサブスクリプションIDのユニーク制約による冪等性保証
- ユニーク制約違反（23505）は正常な重複処理として処理

## パフォーマンス要件

- Webhook応答: 5秒以内

## セキュリティ考慮事項

- Webhookシークレットによる署名検証必須
- 環境変数でシークレット管理（STRIPE_V2_WEBHOOK_SECRET）

## 備考

- Stripe v2 Billing APIを使用（レガシーv1ではない）
- 冪等性はDB制約とコードの両方で保証

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: Webhookエントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | route.ts | `apps/studio.giselles.ai/app/webhooks/stripe/v2/route.ts` | POST関数、署名検証、イベント振り分け |

**主要処理フロー（route.ts）**:
1. **14-36行目**: Webhook受信、署名検証、イベント構築
2. **38-42行目**: 対象イベント判定
3. **44-59行目**: イベントハンドラー呼び出し分岐
4. **61-72行目**: エラーハンドリング

#### Step 2: イベントハンドラーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | handle-pricing-plan-subscription.ts | `apps/studio.giselles.ai/app/webhooks/stripe/handle-pricing-plan-subscription.ts` | サブスクリプション処理ロジック |

**主要処理フロー（handle-pricing-plan-subscription.ts）**:
1. **25-136行目**: handlePricingPlanServicingActivated
   - **27-36行目**: related_objectからサブスクリプションID取得
   - **43-48行目**: Stripe APIでサブスクリプション取得
   - **51-54行目**: ケイデンス取得（顧客ID取得用）
   - **65-87行目**: 冪等性チェック（既存履歴確認）
   - **90-123行目**: メタデータ分岐（アップグレード or 新規作成）
2. **138-162行目**: isActiveSubscriptionIdViolation - ユニーク制約違反判定
3. **167-199行目**: createNewProTeam - 新規Proチーム作成
4. **201-240行目**: upgradeExistingTeam - 既存チームアップグレード
5. **253-283行目**: updateExistingTeam - 冪等性リトライ時の更新
6. **291-369行目**: handlePricingPlanServicingCanceled - キャンセル処理

#### Step 3: 定数を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | constants.ts | `apps/studio.giselles.ai/services/teams/constants.ts` | メタデータキー定義 |

### プログラム呼び出し階層図

```
POST /webhooks/stripe/v2
    │
    ├─ [署名検証]
    │      └─ stripe.webhooks.constructEvent(body, sig, webhookSecret)
    │
    ├─ [イベント分岐]
    │      │
    │      ├─ servicing_activated
    │      │      └─ handlePricingPlanServicingActivated
    │      │             │
    │      │             ├─ stripe.v2.billing.pricingPlanSubscriptions.retrieve
    │      │             ├─ stripe.v2.billing.cadences.retrieve
    │      │             │
    │      │             └─ db.transaction
    │      │                    │
    │      │                    ├─ [冪等性チェック] SELECT履歴
    │      │                    │      └─ 存在 → updateExistingTeam
    │      │                    │
    │      │                    └─ [メタデータ分岐]
    │      │                           ├─ UPGRADING_TEAM_DB_ID → upgradeExistingTeam
    │      │                           └─ DRAFT_TEAM_* → createNewProTeam
    │      │
    │      └─ servicing_canceled
    │             └─ handlePricingPlanServicingCanceled
    │                    │
    │                    ├─ stripe.v2.billing.pricingPlanSubscriptions.retrieve
    │                    ├─ stripe.v2.billing.cadences.retrieve
    │                    │
    │                    └─ db.transaction
    │                           ├─ SELECT履歴
    │                           ├─ UPDATE teams (plan=free)
    │                           └─ INSERT履歴
    │
    └─ Response (200 OK or 400 Error)
```

### データフロー図

```
[Stripe]                        [Giselle]                        [Database]

Webhook ─────────────────────▶ route.ts
                                   │
                                   ├──▶ 署名検証
                                   │
                                   ├──▶ Stripe API呼び出し
                                   │         │
                                   │         ├─ pricingPlanSubscriptions.retrieve
                                   │         └─ cadences.retrieve
                                   │
                                   └──▶ handle-pricing-plan-subscription.ts
                                              │
                                              ├──▶ teams (INSERT/UPDATE)
                                              ├──▶ teamMemberships (INSERT)
                                              └──▶ stripePricingPlanSubscriptionHistories (INSERT)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| route.ts | `apps/studio.giselles.ai/app/webhooks/stripe/v2/route.ts` | ソース | Webhookエントリーポイント |
| handle-pricing-plan-subscription.ts | `apps/studio.giselles.ai/app/webhooks/stripe/handle-pricing-plan-subscription.ts` | ソース | サブスクリプション処理 |
| constants.ts | `apps/studio.giselles.ai/services/teams/constants.ts` | ソース | メタデータキー定義 |
| record-v2-subscription-history.ts | `apps/studio.giselles.ai/services/subscriptions/record-v2-subscription-history.ts` | ソース | 履歴記録 |
| utils.ts | `apps/studio.giselles.ai/services/teams/utils.ts` | ソース | createTeamId |
