# 通知設計書 12-有料サブスクリプション開始通知

## 概要

本ドキュメントは、Ghost CMSにおける有料サブスクリプション開始通知機能の設計仕様を記載したものである。

### 本通知の処理概要

有料サブスクリプション開始通知は、会員が有料プランを開始した際に、スタッフメンバーに対してメール通知を送信する機能である。この通知により、サイト運営者はリアルタイムで有料会員獲得を把握し、収益状況をモニタリングすることができる。

**業務上の目的・背景**：サイト運営者にとって、有料サブスクリプションの獲得は直接的な収益に繋がる最重要イベントである。新規有料会員の獲得状況をリアルタイムで把握することで、プライシング戦略の評価、プロモーション効果の測定、コンテンツ価値の検証が可能となる。特にサブスクリプションビジネスでは、この指標がビジネスの健全性を示す重要なKPIとなる。

**通知の送信タイミング**：有料サブスクリプションがアクティブ化された直後に、`SubscriptionActivatedEvent`ドメインイベントが発火されたタイミングで送信される。新規会員の有料登録だけでなく、無料会員からのアップグレードも対象となる。

**通知の受信者**：Ghostの管理画面で「有料サブスクリプション開始通知」を有効化しているアクティブなスタッフメンバー全員。ユーザーモデルの`paid_subscription_started_notification`カラムが`true`かつ`status`が`active`のユーザーが対象となる。

**通知内容の概要**：会員名、メールアドレス、選択したティア（価格プラン）、適用されたオファー（割引）、サブスクリプション開始日、アトリビューション情報（リファラーソース、登録元ページ）が含まれる。

**期待されるアクション**：受信者は「View member」ボタンから管理画面の会員詳細ページにアクセスし、新規有料会員の情報を確認できる。収益分析やカスタマーサクセス活動に活用することが期待される。

## 通知種別

メール通知（スタッフ向け）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（DomainEventsサブスクライバー経由） |
| 優先度 | 高 |
| リトライ | GhostMailerの標準リトライ機構に依存 |

### 送信先決定ロジック

`models.User.getEmailAlertUsers('paid-started')` メソッドにより、以下の条件を満たすユーザーを取得：
- `status:active` - アクティブなユーザーのみ
- `paid_subscription_started_notification:true` - 有料サブスクリプション開始通知が有効

該当する全ユーザーに対して個別にメールを送信する。

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | サイト設定のデフォルトメールアドレス |
| 送信元名称 | サイト名 |
| 件名 | `Paid subscription started: {会員名}` |
| 形式 | HTML/テキスト両対応 |

### 本文テンプレート

```html
<!-- HTMLテンプレート: new-paid-started.hbs -->
<h1>You have a new paid subscriber</h1>

<p>Name: {{memberData.name}}{{#if memberData.showEmail}} ({{memberData.email}}){{/if}}</p>
<p>Tier: {{tierData.name}}{{#if tierData.details}} - {{tierData.details}}{{/if}}</p>

{{#if offerData}}
  <p>Offer: {{offerData.name}} - <span style="color: {{accentColor}};">{{offerData.details}}</span></p>
{{/if}}

{{#if referrerSource}}
  <p>Source: {{referrerSource}}</p>
  {{#if attributionTitle}}
    <p>Page: <a href="{{attributionUrl}}">{{attributionTitle}}</a></p>
  {{/if}}
{{/if}}

<a href="{{memberData.adminUrl}}">View member</a>
```

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| なし | - | - | 添付ファイルなし |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| memberData.name | 会員の名前 | member.name \|\| member.email | Yes |
| memberData.email | 会員のメールアドレス | member.email | Yes |
| memberData.adminUrl | 管理画面の会員詳細URL | urlUtils.urlFor('admin') + '/members/{id}' | Yes |
| tierData.name | ティア（価格プラン）名 | tier.name | Yes |
| tierData.details | ティア詳細（価格/間隔） | `${formattedAmount}/${interval}` | No |
| offerData.name | オファー名 | offer.name | No |
| offerData.details | オファー詳細 | `${offAmount}${offDuration}` | No |
| subscriptionData.startedOn | 開始日 | moment(subscription.startDate).format('D MMM YYYY') | Yes |
| referrerSource | リファラーソース | attribution.referrerSource | No |
| attributionTitle | 登録元ページタイトル | attribution.title | No |
| attributionUrl | 登録元ページURL | attribution.url | No |
| siteTitle | サイト名 | settingsCache.get('title') | Yes |
| siteIconUrl | サイトアイコンURL | blogIcon.getIconUrl() | No |
| accentColor | アクセントカラー | settingsCache.get('accent_color') | No |
| staffUrl | スタッフ設定URL | urlUtils.urlFor('admin') + '/settings/staff/{slug}/email-notifications' | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| ドメインイベント | SubscriptionActivatedEvent | source in ['api', 'member'] | 有料サブスクリプションがアクティブ化された際に発火 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| ソースが'api'または'member'以外 | インポートなど別ソースからの操作では送信しない |
| 通知対象ユーザーが0人 | `paid_subscription_started_notification:true`のユーザーがいない場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[SubscriptionActivatedEvent発火] --> B{ソースチェック}
    B -->|api/member| C[会員・ティア・サブスクリプション情報取得]
    B -->|その他| Z[処理終了]
    C --> D[アトリビューション情報取得]
    D --> E[通知対象ユーザー取得]
    E --> F{対象ユーザー存在?}
    F -->|Yes| G[各ユーザーにメール送信]
    F -->|No| Z
    G --> H[金額フォーマット・オファー計算]
    H --> I[テンプレートデータ構築]
    I --> J[HTML/テキストテンプレート展開]
    J --> K[GhostMailerでメール送信]
    K --> Z
```

## データベース参照・更新仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| members | 会員情報取得 | id, name, email, geolocation |
| products | ティア情報取得 | id, name |
| stripe_customer_subscriptions | サブスクリプション情報取得 | plan情報含む |
| offers | オファー情報取得 | discount_type, discount_amount, duration等 |
| users | 通知対象スタッフ取得 | status:active, paid_subscription_started_notification:true |
| settings | サイト設定取得 | title, accent_color など |

### テーブル別参照項目詳細

#### stripe_customer_subscriptions

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | 識別子 | イベントから取得 |
| plan.amount | 金額（セント単位） | - |
| plan.interval | 課金間隔（month/year） | - |
| plan.currency | 通貨コード | - |
| start_date | 開始日 | - |

#### offers

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| name | オファー名 | オファーIDから取得 |
| discount_type | 割引タイプ（percent/fixed/trial） | - |
| discount_amount | 割引額 | - |
| duration | 適用期間（once/repeating/forever/trial） | - |
| duration_in_months | 月数 | durationがrepeatingの場合 |
| currency | 通貨 | discount_typeがfixedの場合 |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| なし | - | この通知では更新処理なし |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | SMTP接続エラー等 | logging.errorでログ出力、処理継続 |
| アトリビューション取得失敗 | memberAttributionServiceエラー | logging.warnでログ出力、attributionなしで送信継続 |
| データ取得失敗 | 会員/ティア/サブスクリプション不在 | エラーをスロー |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | GhostMailerのデフォルト設定に依存 |
| リトライ間隔 | GhostMailerのデフォルト設定に依存 |
| リトライ対象エラー | 一時的なSMTPエラー |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし |
| 1日あたり上限 | 制限なし |

### 配信時間帯

制限なし（イベント発生時に即時送信）

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

- 決済情報（金額、オファー詳細）が含まれるため、送信先はスタッフメンバーのみに限定
- Stripe関連の詳細情報（subscription_id等）は直接含まれていない
- 管理画面へのURLは認証必須のため、URLが漏洩しても直接アクセスは不可

## 備考

- 金額は`amount / 100`で計算（Stripeはセント単位）
- オファーの割引詳細は動的に計算（percent: N% off、fixed: $N off、trial: N days free）
- `homepage`というアトリビューションタイトルは表示時に`Homepage`に変換される

---

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

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

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | staff-service.js | `ghost/core/core/server/services/staff/staff-service.js` | 31-63行目: member, tier, subscription, offerのシリアライズ構造 |

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | staff-service.js | `ghost/core/core/server/services/staff/staff-service.js` | 150-157行目: SubscriptionActivatedEventサブスクリプション |
| 2-2 | staff-service.js | `ghost/core/core/server/services/staff/staff-service.js` | 112-129行目: handleEventの有料サブスクリプション処理 |

**主要処理フロー**:
1. **150行目**: `SubscriptionActivatedEvent`をサブスクライブ
2. **112-129行目**: アトリビューション取得 → `notifyPaidSubscriptionStarted`呼び出し

#### Step 3: メール送信処理

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | staff-service-emails.js | `ghost/core/core/server/services/staff/staff-service-emails.js` | 68-126行目: `notifyPaidSubscriptionStarted`メソッド |
| 3-2 | staff-service-emails.js | `ghost/core/core/server/services/staff/staff-service-emails.js` | 389-418行目: `getOfferData`メソッド（オファー詳細計算） |

**主要処理フロー**:
- **77-78行目**: 金額計算（getAmount, getFormattedAmount）
- **89行目**: オファーデータ取得
- **117行目**: テンプレートレンダリング

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

```
SubscriptionActivatedEvent（ドメインイベント発火）
    │
    └─ StaffService.handleEvent()
           │
           ├─ getDataFromIds() - 会員・ティア・サブスク・オファーデータ取得
           │      ├─ models.Member.findOne()
           │      ├─ models.Product.findOne()
           │      ├─ models.StripeCustomerSubscription.findOne()
           │      └─ models.Offer.findOne()
           │
           ├─ memberAttributionService.getSubscriptionCreatedAttribution()
           │
           └─ StaffServiceEmails.notifyPaidSubscriptionStarted()
                  │
                  ├─ models.User.getEmailAlertUsers('paid-started')
                  ├─ getAmount() / getFormattedAmount()
                  ├─ getOfferData()
                  ├─ renderEmailTemplate('new-paid-started')
                  └─ sendMail()
```

### データフロー図

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

SubscriptionActivatedEvent ───▶ StaffService ───▶ スタッフメンバーへのメール
  │                                │
  ├─ memberId                     ├─ 会員データ取得
  ├─ tierId                       ├─ ティアデータ取得
  ├─ subscriptionId               ├─ サブスクリプションデータ取得
  ├─ offerId                      ├─ オファーデータ取得
  ├─ source                       ├─ アトリビューション取得
  └─ attribution                  ├─ 金額・オファー詳細計算
                                  └─ メール送信
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| staff-service.js | `ghost/core/core/server/services/staff/staff-service.js` | ソース | イベントハンドリング |
| staff-service-emails.js | `ghost/core/core/server/services/staff/staff-service-emails.js` | ソース | メール送信ロジック |
| new-paid-started.hbs | `ghost/core/core/server/services/staff/email-templates/new-paid-started.hbs` | テンプレート | HTMLメールテンプレート |
| new-paid-started.txt.js | `ghost/core/core/server/services/staff/email-templates/new-paid-started.txt.js` | テンプレート | テキストメールテンプレート |
