# 通知設計書 13-サブスクリプションキャンセル通知

## 概要

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

### 本通知の処理概要

サブスクリプションキャンセル通知は、有料会員がサブスクリプションをキャンセルした際に、スタッフメンバーに対してメール通知を送信する機能である。この通知により、サイト運営者は会員の解約状況をリアルタイムで把握し、リテンション施策の検討や解約理由の分析に活用できる。

**業務上の目的・背景**：サブスクリプションビジネスにおいて、解約は収益に直接影響する重要なイベントである。解約の早期検知により、運営者はリテンション施策（再エンゲージメントメール、特別オファーなど）を検討できる。また、解約理由が記録されている場合は、サービス改善のためのフィードバックとして活用できる。チャーン率（解約率）のモニタリングはサブスクリプションビジネスの健全性を測る重要な指標である。

**通知の送信タイミング**：会員がサブスクリプションのキャンセルを実行した直後に、`SubscriptionCancelledEvent`ドメインイベントが発火されたタイミングで送信される。即時キャンセルと期間終了時キャンセルの両方が対象。

**通知の受信者**：Ghostの管理画面で「サブスクリプションキャンセル通知」を有効化しているアクティブなスタッフメンバー全員。ユーザーモデルの`paid_subscription_canceled_notification`カラムが`true`かつ`status`が`active`のユーザーが対象。なお、このオプションはデフォルトでは`false`。

**通知内容の概要**：会員名、メールアドレス、解約したティア（価格プラン）、有効期限（満了日）、キャンセル理由（記録されている場合）が含まれる。

**期待されるアクション**：受信者は「View member」ボタンから管理画面の会員詳細ページにアクセスし、解約した会員へのフォローアップ（再エンゲージメントの試み、フィードバック収集など）を検討できる。

## 通知種別

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

## 送信仕様

### 基本情報

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

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

`models.User.getEmailAlertUsers('paid-canceled')` メソッドにより、以下の条件を満たすユーザーを取得：
- `status:active` - アクティブなユーザーのみ
- `paid_subscription_canceled_notification:true` - キャンセル通知が有効（デフォルトはfalse）

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

## 通知テンプレート

### メール通知の場合

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

### 本文テンプレート

```html
<!-- HTMLテンプレート: new-paid-cancellation.hbs -->
<h1>A paid member's subscription has just been canceled</h1>

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

{{#if subscriptionData.cancelNow}}
  <p>Expired on: {{subscriptionData.expiryAt}}</p>
{{else}}
  <p>Expires on: {{subscriptionData.expiryAt}}</p>
{{/if}}

{{#if subscriptionData.cancellationReason}}
  <p>"{{subscriptionData.cancellationReason}}"</p>
{{/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 |
| subscriptionData.expiryAt | 有効期限（フォーマット済み） | getFormattedDate(expiryAt) | Yes |
| subscriptionData.cancelNow | 即時キャンセルフラグ | cancelNow (イベントデータ) | Yes |
| subscriptionData.canceledAt | キャンセル日（フォーマット済み） | getFormattedDate(canceledAt) | No |
| subscriptionData.cancellationReason | キャンセル理由 | subscription.cancellation_reason | 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 |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| ドメインイベント | SubscriptionCancelledEvent | source in ['api', 'member'] | サブスクリプションがキャンセルされた際に発火 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| ソースが'api'または'member'以外 | インポートなど別ソースからの操作では送信しない |
| 通知対象ユーザーが0人 | デフォルトでfalseのため、明示的に有効化したユーザーのみ対象 |

## 処理フロー

### 送信フロー

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

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| members | 会員情報取得 | id, name, email |
| products | ティア情報取得 | id, name |
| stripe_customer_subscriptions | サブスクリプション情報取得 | plan情報、cancellation_reason |
| users | 通知対象スタッフ取得 | status:active, paid_subscription_canceled_notification:true |
| settings | サイト設定取得 | title, accent_color |

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

#### stripe_customer_subscriptions

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| plan.amount | 金額 | - |
| plan.interval | 課金間隔 | - |
| plan.currency | 通貨 | - |
| current_period_end | 有効期限 | cancelAtとして使用 |
| cancellation_reason | キャンセル理由 | - |

### 更新テーブル一覧

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

## エラー処理

### エラーケース一覧

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

### リトライ仕様

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

## 配信設定

### レート制限

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

### 配信時間帯

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

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

- 決済情報とキャンセル理由が含まれるため、送信先はスタッフメンバーのみに限定
- キャンセル理由には顧客の個人的なフィードバックが含まれる可能性があるため、適切なアクセス制御が必要
- デフォルトでこの通知はオフになっており、明示的なオプトインが必要

## 備考

- `paid_subscription_canceled_notification`のデフォルト値は`false`（他の通知と異なる）
- `cancelNow`フラグにより、即時キャンセル（Expired on）と期間終了時キャンセル（Expires on）を区別
- キャンセル理由はStripeから取得されるオプショナルなフィールド

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | staff-service.js | `ghost/core/core/server/services/staff/staff-service.js` | 31-63行目: subscriptionのシリアライズ（特にcancelAt, cancellationReason） |
| 1-2 | user.js | `ghost/core/core/server/models/user.js` | 70行目: paid_subscription_canceled_notificationのデフォルト値がfalse |

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | staff-service.js | `ghost/core/core/server/services/staff/staff-service.js` | 159-166行目: SubscriptionCancelledEventサブスクリプション |
| 2-2 | staff-service.js | `ghost/core/core/server/services/staff/staff-service.js` | 130-137行目: handleEventのキャンセル処理 |

**主要処理フロー**:
1. **130行目**: `SubscriptionCancelledEvent`を検出
2. **131-136行目**: `notifyPaidSubscriptionCanceled`を呼び出し（イベントデータをスプレッド展開）

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | staff-service-emails.js | `ghost/core/core/server/services/staff/staff-service-emails.js` | 128-177行目: `notifyPaidSubscriptionCanceled`メソッド |

**主要処理フロー**:
- **129行目**: getEmailAlertUsers('paid-canceled')で対象取得
- **134行目**: 件名に警告絵文字を使用
- **145-150行目**: subscriptionDataにexpiryAt, cancelNow, canceledAt, cancellationReasonを設定
- **168行目**: テンプレートレンダリング

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

```
SubscriptionCancelledEvent（ドメインイベント発火）
    │
    └─ StaffService.handleEvent()
           │
           ├─ getDataFromIds() - 会員・ティア・サブスクデータ取得
           │
           └─ StaffServiceEmails.notifyPaidSubscriptionCanceled()
                  │
                  ├─ models.User.getEmailAlertUsers('paid-canceled')
                  ├─ getMemberData()
                  ├─ getAmount() / getFormattedAmount()
                  ├─ getFormattedDate() - expiryAt/canceledAt
                  ├─ renderEmailTemplate('new-paid-cancellation')
                  └─ sendMail()
```

### データフロー図

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

SubscriptionCancelledEvent ───▶ StaffService ───▶ スタッフメンバーへのメール
  │                                │
  ├─ memberId                     ├─ 会員データ取得
  ├─ tierId                       ├─ ティアデータ取得
  ├─ subscriptionId               ├─ サブスクリプションデータ取得
  ├─ cancelNow                    ├─ 日付フォーマット
  ├─ expiryAt                     └─ メール送信
  └─ canceledAt
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| 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-cancellation.hbs | `ghost/core/core/server/services/staff/email-templates/new-paid-cancellation.hbs` | テンプレート | HTMLメールテンプレート |
| new-paid-cancellation.txt.js | `ghost/core/core/server/services/staff/email-templates/new-paid-cancellation.txt.js` | テンプレート | テキストメールテンプレート |
| events.js | `ghost/core/core/shared/events.js` | ソース | SubscriptionCancelledEventの定義 |
