# 機能設計書 16-オファー管理

## 概要

本ドキュメントは、Ghostにおけるオファー管理機能の設計仕様を記載する。本機能は、割引クーポン・トライアル期間などのプロモーションオファーを作成・管理する機能を提供する。

### 本機能の処理概要

**業務上の目的・背景**：
サイト運営者がメンバー獲得を促進するためには、期間限定の割引や無料トライアルなどのプロモーション施策が有効である。オファー機能により、パーセント割引、固定金額割引、無料トライアル期間などの多様なプロモーションを設定し、専用URLで配布できる。また、既存購読者の継続率向上のためのリテンションオファーにも対応する。

**機能の利用シーン**：
- 新規購読者向けの割引キャンペーンを作成する場合
- 特定イベント用のプロモーションコードを発行する場合
- 年額プランへの移行促進割引を設定する場合
- 解約予防用のリテンションオファーを作成する場合

**主要な処理内容**：
1. オファーの作成（名前、コード、タイプ、金額、期間設定）
2. オファーの取得（一覧/個別）
3. オファーの更新（名前、コード、表示情報、ステータス）
4. Stripeクーポンとの連携
5. オファー適用状況の追跡

**関連システム・外部連携**：
- Stripe API（クーポン作成・管理）
- Tier管理（オファー対象Tier）
- チェックアウト処理（オファー適用）

**権限による制御**：
- オファー設定の変更: Administrator以上

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 56 | オファー設定 | 主画面 | 割引・クーポンオファーの作成・管理 |
| 87 | オファーページ | 主画面 | 特別オファーの表示・適用 |

## 機能種別

CRUD操作 / プロモーション管理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | オファー名（内部識別用） | 191文字以内、ユニーク |
| code | string | Yes | オファーコード（URL用） | 191文字以内、ユニーク |
| display_title | string | No | 表示タイトル | 191文字以内 |
| display_description | string | No | 表示説明 | - |
| type | string | Yes | 割引タイプ | percent/fixed/trial |
| amount | number | Yes (percent/fixed) | 割引額（パーセントまたはセント） | 正の整数 |
| duration | string | Yes | 適用期間 | once/repeating/forever/trial |
| duration_in_months | number | Cond. | 繰り返し月数 | duration=repeatingの時必須 |
| cadence | string | Yes | 課金周期 | month/year |
| tier | object | Yes | 対象Tier | id必須 |
| currency | string | Cond. | 通貨コード | type=fixedの時必須 |
| redemption_type | string | No | 適用タイプ | signup/retention |
| status | string | No | ステータス | active/archived |

### 入力データソース

- 管理画面からのAPI呼び出し

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | オファーID |
| name | string | オファー名 |
| code | string | オファーコード |
| display_title | string | 表示タイトル |
| display_description | string | 表示説明 |
| type | string | percent/fixed/trial |
| amount | number | 割引額 |
| duration | string | 適用期間 |
| duration_in_months | number | 繰り返し月数 |
| cadence | string | 課金周期 |
| tier | object | 対象Tier |
| currency | string | 通貨コード |
| redemption_type | string | 適用タイプ |
| status | string | ステータス |
| redemption_count | number | 適用回数 |
| created_at | Date | 作成日時 |

### 出力先

- REST API レスポンス（JSON形式）
- DBテーブル（offers）

## 処理フロー

### 処理シーケンス

```
1. APIリクエスト受信
   └─ OffersAPIでリクエスト処理
2. バリデーション
   └─ Offerドメインモデルでバリデーション
3. ユニークチェック
   └─ name, code の重複チェック
4. Stripeクーポン作成（新規作成時）
   └─ Stripe APIでクーポン作成
5. ドメインモデル操作
   └─ Offer.create() または offer.update*()
6. 永続化
   └─ OfferRepositoryでDB保存
7. イベント発火
   └─ OfferCreatedEvent / OfferCodeChangeEvent
```

### フローチャート

```mermaid
flowchart TD
    A[APIリクエスト] --> B{操作種別}
    B -->|listOffers| C[getAll]
    B -->|getOffer| D[getById]
    B -->|createOffer| E[ユニークチェック]
    B -->|updateOffer| F[getById + 更新]
    E -->|OK| G[Offer.create]
    E -->|NG| H[ValidationError]
    G --> I[Stripeクーポン作成]
    I --> J[repository.save]
    F --> J
    J --> K[イベント発火]
    K --> L[レスポンス返却]
    C --> L
    D --> L
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-16-01 | コードユニーク | オファーコードは全オファーでユニーク | 作成・更新時 |
| BR-16-02 | 名前ユニーク | オファー名は全オファーでユニーク | 作成・更新時 |
| BR-16-03 | パーセント上限 | type=percentの場合、amountは100以下 | 作成時 |
| BR-16-04 | トライアル設定 | type=trialの場合、durationはtrial固定 | 作成時 |
| BR-16-05 | Tier固定 | 作成後はTierの変更不可 | 更新時 |
| BR-16-06 | タイプ固定 | 作成後はtypeの変更不可 | 更新時 |
| BR-16-07 | リテンションオファー | redemption_type=retentionは既存購読者向け | 作成時 |
| BR-16-08 | Stripeクーポン連携 | オファー作成時にStripeクーポンを自動作成 | 作成時 |

### 計算ロジック

- パーセント割引: 元価格 * (1 - amount/100)
- 固定金額割引: 元価格 - amount

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| オファー作成 | offers | INSERT | 新規オファーレコード |
| オファー更新 | offers | UPDATE | オファー情報更新 |
| 適用記録 | offer_redemptions | INSERT | オファー適用履歴 |

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

#### offers

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | ObjectID | 自動生成 |
| INSERT | name | オファー名 | ユニーク |
| INSERT | code | オファーコード | ユニーク |
| INSERT | display_title | 表示タイトル | nullable |
| INSERT | display_description | 表示説明 | nullable |
| INSERT | type | percent/fixed/trial | - |
| INSERT | amount | 割引額 | - |
| INSERT | duration | once/repeating/forever/trial | - |
| INSERT | duration_in_months | 繰り返し月数 | nullable |
| INSERT | cadence | month/year | - |
| INSERT | tier_id | 対象TierID | FK |
| INSERT | currency | 通貨コード | nullable |
| INSERT | redemption_type | signup/retention | - |
| INSERT | status | active/archived | デフォルト: active |
| INSERT | stripe_coupon_id | StripeクーポンID | - |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| ValidationError | 400 | 名前が既に存在 | 別の名前を使用 |
| ValidationError | 400 | コードが既に存在 | 別のコードを使用 |
| ValidationError | 400 | パーセントが100超過 | 100以下に設定 |
| NotFoundError | 404 | 指定IDのオファーなし | 正しいIDを指定 |

### リトライ仕様

特になし（同期処理のため）

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

- オファー作成: Stripeクーポン作成とDB保存をトランザクションで実行

## パフォーマンス要件

- オファー一覧取得: フィルタリング対応
- 適用可能オファー検索: Tier・cadence・redemption_typeでフィルタ

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

- 認証必須: Admin API アクセストークン
- オファーコードの推測困難性

## 備考

- Stripeクーポンからのオファー自動作成（ensureOfferForStripeCoupon）にも対応

---

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

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

### 推奨読解順序

#### Step 1: ドメインモデルを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | offer.js | `ghost/core/core/server/services/offers/domain/models/offer.js` | Offerドメインモデル |
| 1-2 | offer-type.js | `ghost/core/core/server/services/offers/domain/models/offer-type.js` | タイプ定義 |
| 1-3 | offer-duration.js | `ghost/core/core/server/services/offers/domain/models/offer-duration.js` | 期間定義 |

**読解のコツ**:
- ValueObjectパターンでの型安全なモデリング
- create()でのバリデーションとイベント生成
- updateName/updateCodeでのユニークチェック

#### Step 2: API層を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | offers-api.js | `ghost/core/core/server/services/offers/application/offers-api.js` | APIメソッド |

**主要処理フロー**:
- **33-49行目**: getOffer() - 個別取得
- **57-67行目**: createOffer() - 新規作成
- **81-121行目**: updateOffer() - 更新処理
- **128-136行目**: listOffers() - 一覧取得
- **146-212行目**: listOffersAvailableToSubscription() - 適用可能オファー検索
- **229-270行目**: ensureOfferForStripeCoupon() - Stripeからの自動作成

#### Step 3: リポジトリを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | offer-bookshelf-repository.js | `ghost/core/core/server/services/offers/offer-bookshelf-repository.js` | DB操作 |

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

```
OffersAPI
    │
    ├─ getOffer()
    │      └─ repository.getById()
    │
    ├─ createOffer()
    │      ├─ UniqueChecker
    │      ├─ Offer.create()
    │      │      └─ OfferCreatedEvent
    │      └─ repository.save()
    │
    ├─ updateOffer()
    │      ├─ repository.getById()
    │      ├─ offer.updateName() / updateCode()
    │      │      └─ OfferCodeChangeEvent
    │      └─ repository.save()
    │
    ├─ listOffers()
    │      └─ repository.getAll()
    │
    └─ listOffersAvailableToSubscription()
           ├─ repository.getAll()
           └─ repository.getRedeemedOfferIdsForSubscription()
```

### データフロー図

```
[入力]                     [処理]                           [出力]

管理画面              OffersAPI           Offer (Domain)
リクエスト ────▶ (API Layer) ────▶ (Domain Model)
   │                   │                    │
   │                   │                    ├─ バリデーション
   │                   │                    ├─ ユニークチェック
   │                   │                    └─ イベント生成
   │                   │
   │                   ▼
   │             OfferRepository ─────────▶ offers テーブル
   │                   │
   │                   ▼
   │             Stripe API ─────────────▶ Stripe Coupon
   │
   └── JSON ◀─────── レスポンス
       Response
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| offers-api.js | `ghost/core/core/server/services/offers/application/offers-api.js` | ソース | API層 |
| offer.js | `ghost/core/core/server/services/offers/domain/models/offer.js` | ソース | ドメインモデル |
| offer-bookshelf-repository.js | `ghost/core/core/server/services/offers/offer-bookshelf-repository.js` | ソース | リポジトリ |
| offer-mapper.js | `ghost/core/core/server/services/offers/application/offer-mapper.js` | ソース | DTOマッパー |
| unique-checker.js | `ghost/core/core/server/services/offers/application/unique-checker.js` | ソース | ユニークチェック |
| offer-type.js | `ghost/core/core/server/services/offers/domain/models/offer-type.js` | ソース | タイプ定義 |
| offer-duration.js | `ghost/core/core/server/services/offers/domain/models/offer-duration.js` | ソース | 期間定義 |
| offer-created-event.js | `ghost/core/core/server/services/offers/domain/events/offer-created-event.js` | ソース | 作成イベント |
| offer-code-change-event.js | `ghost/core/core/server/services/offers/domain/events/offer-code-change-event.js` | ソース | コード変更イベント |
