# 通知設計書 46-member.added

## 概要

本ドキュメントは、Ghost CMSにおける `member.added` Webhookイベントの設計仕様を定義する。このWebhookは、新しい会員が登録された際に、登録されたWebhookエンドポイントに通知を送信する機能を提供する。

### 本通知の処理概要

このWebhookは、Ghost CMSで新規会員が追加された際に、外部システムへリアルタイムで通知を送信する機能を提供する。

**業務上の目的・背景**：会員の追加は、Ghost CMSにおける重要なビジネスイベントである。新規会員の登録を外部システム（CRM、メールマーケティングツール、分析プラットフォーム、課金システムなど）に通知することで、会員管理の自動化、ウェルカムメールキャンペーンのトリガー、顧客データの同期などが可能になる。特にサブスクリプションビジネスにおいて、会員の獲得をリアルタイムで追跡することは重要である。

**通知の送信タイミング**：Memberモデルの `onCreated` フックが呼び出された際に `member.added` イベントが発火される。これは会員の新規作成（サインアップフォーム、Admin API、管理画面での手動追加、インポートなど）が完了したタイミングで発生する。ただし、インポート処理中（options.importing=true）の場合はWebhookは送信されない。

**通知の受信者**：Ghost管理画面で設定された `member.added` イベントに対応するWebhookエンドポイント。これらはIntegration（連携機能）を通じて登録される外部システムのURLである。

**通知内容の概要**：作成された会員の全情報を含むJSONペイロード。会員の基本情報（ID、UUID、メールアドレス、名前、ステータスなど）およびサブスクリプション情報が含まれる。

**期待されるアクション**：受信側システムは、新規会員の通知を受け取り、CRMへの登録、ウェルカムキャンペーンの開始、顧客データベースの更新、分析イベントの記録などの処理を実行することが期待される。

## 通知種別

Webhook（HTTP POST リクエスト）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（イベント駆動） |
| 優先度 | 中 |
| リトライ | 有・5回（テスト環境では0回） |

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

1. `webhooks` テーブルから `event = 'member.added'` に一致するレコードを取得
2. カスタムインテグレーション制限（`customIntegrations` limit）が有効な場合は、内部インテグレーション（`type = 'internal'`）に紐づくWebhookのみを対象とする
3. 各Webhookの `target_url` に対してHTTP POSTリクエストを送信

## 通知テンプレート

### Webhook通知の場合

| 項目 | 内容 |
|-----|------|
| HTTPメソッド | POST |
| Content-Type | application/json |
| Content-Version | v{Ghost APIバージョン} |
| タイムアウト | 2000ms（2秒） |

### ペイロード構造

```json
{
  "member": {
    "current": {
      "id": "会員ID",
      "uuid": "会員UUID",
      "email": "メールアドレス",
      "name": "会員名",
      "note": "メモ",
      "status": "free",
      "geolocation": "位置情報",
      "email_count": 0,
      "email_opened_count": 0,
      "email_open_rate": null,
      "enable_comment_notifications": true,
      "last_seen_at": null,
      "created_at": "作成日時",
      "updated_at": "更新日時"
    },
    "previous": {}
  }
}
```

### 署名ヘッダー（オプション）

Webhookに `secret` が設定されている場合、以下のヘッダーが追加される：

| ヘッダー名 | 形式 |
|----------|------|
| X-Ghost-Signature | `sha256={HMAC署名}, t={タイムスタンプ}` |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| member.current.id | 会員の一意識別子 | members.id | Yes |
| member.current.uuid | 会員のUUID | members.uuid | Yes |
| member.current.email | メールアドレス | members.email | Yes |
| member.current.name | 会員名 | members.name | No |
| member.current.note | 管理者用メモ | members.note | No |
| member.current.status | 会員ステータス（free/paid/comped） | members.status | Yes |
| member.current.geolocation | 位置情報 | members.geolocation | No |
| member.current.email_count | 受信メール数 | members.email_count | Yes |
| member.current.email_opened_count | 開封メール数 | members.email_opened_count | Yes |
| member.current.email_open_rate | メール開封率 | members.email_open_rate | No |
| member.current.enable_comment_notifications | コメント通知設定 | members.enable_comment_notifications | Yes |
| member.current.last_seen_at | 最終アクセス日時 | members.last_seen_at | No |
| member.current.created_at | 作成日時 | members.created_at | Yes |
| member.previous | 変更前の値（新規作成のため空オブジェクト） | - | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | 管理画面で会員を手動追加 | Member.onCreated発火 | Members > 新規追加 |
| API | Admin API経由で会員作成 | Member.onCreated発火 | POST /admin/members |
| サインアップ | フロントエンドからの会員登録 | Member.onCreated発火 | Portal経由での登録 |
| インポート | CSV/JSONからの一括インポート | 送信しない | options.importing=trueで抑止 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| options.importing === true | インポート処理中は送信しない |
| 対象Webhookが存在しない | member.addedイベントに登録されたWebhookがない場合 |
| customIntegrations制限超過 | カスタムインテグレーション数制限を超えている場合は内部インテグレーションのみ |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[会員作成操作] --> B{インポート中?}
    B -->|Yes| Z[処理終了]
    B -->|No| C[Member.onCreated発火]
    C --> D[emitChange'added'呼び出し]
    D --> E[member.addedイベント発火]
    E --> F[WebhookTrigger.trigger呼び出し]
    F --> G[Webhook.findAllByEvent実行]
    G --> H{対象Webhook存在?}
    H -->|No| Z
    H -->|Yes| I[ペイロード生成]
    I --> J[各Webhookに対してHTTP POST]
    J --> K{送信成功?}
    K -->|Yes| L[last_triggered_at更新]
    K -->|No| M{ステータス410?}
    M -->|Yes| N[Webhook削除]
    M -->|No| O[last_triggered_error記録]
    L --> Z
    N --> Z
    O --> Z
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| webhooks | 対象Webhookの取得 | event='member.added' |
| integrations | Webhook連携先の確認 | 内部インテグレーション判定用 |
| members | 会員データの取得 | 新規作成された会員 |
| labels | ラベル情報の取得 | 会員に関連付けられたラベル |

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

#### webhooks

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | Webhook識別 | - |
| event | イベント種別 | event = 'member.added' |
| target_url | 送信先URL | - |
| secret | 署名生成用シークレット | NULLでない場合のみ使用 |
| integration_id | 連携先の識別 | インテグレーション制限判定用 |

#### members

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id, uuid, email | 会員基本情報 | 作成された会員 |
| name, note | 会員詳細情報 | - |
| status | 会員ステータス | free/paid/comped |
| geolocation | 位置情報 | - |
| email_count, email_opened_count | メール統計 | - |
| enable_comment_notifications | 通知設定 | - |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| webhooks | UPDATE | 送信結果の記録 |

#### 送信結果更新

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| UPDATE | last_triggered_at | Date.now() | 送信日時 |
| UPDATE | last_triggered_status | HTTPステータスコード | 成功/失敗時 |
| UPDATE | last_triggered_error | エラーメッセージまたはNULL | 失敗時のみ |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| タイムアウト | 2秒以内に応答なし | リトライ実行 |
| 410 Gone | 受信側がWebhook削除を要求 | Webhookレコードを削除 |
| 接続エラー | ネットワーク障害 | リトライ実行、エラーログ記録 |
| 5xx エラー | サーバー側エラー | リトライ実行、エラーログ記録 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 5回（本番環境）、0回（テスト環境） |
| リトライ間隔 | @tryghost/requestのデフォルト設定に従う |
| リトライ対象エラー | タイムアウト、接続エラー、5xxエラー |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし（イベント発生ごとに送信） |
| 1日あたり上限 | 制限なし |

### 配信時間帯

送信可能な時間帯の制限なし。イベント発生時に即座に送信される。

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

- **署名検証**: Webhookに `secret` が設定されている場合、`X-Ghost-Signature` ヘッダーでHMAC-SHA256署名を提供
- **HTTPS推奨**: `target_url` にはHTTPS URLの使用を推奨
- **個人情報**: 会員のメールアドレスや名前などの個人情報がペイロードに含まれるため、受信側での適切な取り扱いが必要
- **タイムスタンプ**: 署名にタイムスタンプを含めることでリプレイ攻撃を防止

## 備考

- 会員のデフォルトステータスは `free`（member.js 15行目）
- UUIDとtransient_idは自動生成される（member.js 16-17行目）
- 会員追加時、デフォルトでコメント通知は有効（enable_comment_notifications=true）
- インポート時はWebhookが発火しないため、大量の会員インポート時のWebhook過負荷を防止

---

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

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

### 推奨読解順序

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

Webhookと会員の基本構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | schema.js | `ghost/core/core/server/data/schema/schema.js` | webhooksテーブル（353-370行目）とmembersテーブル（417-444行目）の定義を確認 |
| 1-2 | member.js | `ghost/core/core/server/models/member.js` | Memberモデルの構造、defaults、relationships |

**読解のコツ**: membersテーブルのstatusカラム（free/paid/comped）とdefaultsでの初期値設定を理解する。

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

イベント発火の起点を特定する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | member.js | `ghost/core/core/server/models/member.js` | `onCreated`メソッド（255-259行目）と`emitChange`メソッド（250-253行目） |

**主要処理フロー**:
1. **255行目**: `onCreated`メソッドの開始
2. **256行目**: 親クラスの`onCreated`を呼び出し
3. **258行目**: `emitChange('added', options)` でイベント発火
4. **250-252行目**: `emitChange`で `'member.' + 'added'` = `'member.added'` イベントを構成

#### Step 3: イベントリスナーの登録を理解する

Webhookシステムがどのようにイベントを購読するか。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | listen.js | `ghost/core/core/server/services/webhooks/listen.js` | WEBHOOKSリスト（10-45行目）と`listen`関数（47-68行目） |

**主要処理フロー**:
- **37行目**: `member.added` がWEBHOOKSリストに定義
- **59-66行目**: `events.on`でイベントリスナーを登録、`webhookTrigger.trigger`を呼び出し
- **61-63行目**: `options.importing` の場合は早期リターン

#### Step 4: Webhook送信処理を理解する

実際のHTTPリクエスト送信ロジック。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | webhook-trigger.js | `ghost/core/core/server/services/webhooks/webhook-trigger.js` | `trigger`メソッド（98-143行目） |
| 4-2 | serialize.js | `ghost/core/core/server/services/webhooks/serialize.js` | リソースのシリアライズ処理 |

**主要処理フロー**:
- **serialize.js 7行目**: `resourceName = event.match(/(\w+)\./)[1]` で'member'を抽出
- **serialize.js 8行目**: `docName = 'members'` としてAPIシリアライザーを使用

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

```
Member.onCreated (models/member.js:255)
    │
    └─ emitChange('added') (models/member.js:258)
           │
           └─ events.emit('member.added') (lib/common/events.js)
                  │
                  └─ processWebhookTrigger (services/webhooks/listen.js:59)
                         │
                         ├─ options.importing? → return (抑止)
                         │
                         └─ WebhookTrigger.trigger (services/webhooks/webhook-trigger.js:98)
                                │
                                ├─ getAll('member.added') (webhook-trigger.js:23)
                                │      └─ Webhook.findAllByEvent (models/webhook.js:46)
                                │
                                ├─ payload() (services/webhooks/payload.js)
                                │      └─ serialize() (services/webhooks/serialize.js)
                                │             └─ API serializers (members)
                                │
                                └─ request(url, opts) (HTTP送信)
```

### データフロー図

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

会員作成                 Member.onCreated
(登録/API/管理画面)────▶ emitChange('added')  ────▶   イベント発火
                              │                         │
                              │ (importing時は抑止)      │
                              │                         ▼
                        member.added              WebhookTrigger
                        イベント                       │
                              │                        ▼
                              │                   findAllByEvent
                              │                   (webhooksテーブル)
                              │                        │
                              ▼                        ▼
                        serialize()               HTTP POST
                        (ペイロード生成)          (target_url)
                              │                        │
                              ▼                        ▼
                        {member: {                外部システム
                          current: {
                            email, name, status, ...
                          },
                          previous: {}
                        }}
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| listen.js | `ghost/core/core/server/services/webhooks/listen.js` | ソース | イベントリスナー登録 |
| webhook-trigger.js | `ghost/core/core/server/services/webhooks/webhook-trigger.js` | ソース | Webhook送信制御 |
| payload.js | `ghost/core/core/server/services/webhooks/payload.js` | ソース | ペイロード生成エントリ |
| serialize.js | `ghost/core/core/server/services/webhooks/serialize.js` | ソース | リソースシリアライズ |
| member.js | `ghost/core/core/server/models/member.js` | ソース | Memberモデル・イベント発火 |
| webhook.js | `ghost/core/core/server/models/webhook.js` | ソース | Webhookモデル定義 |
| events.js | `ghost/core/core/server/lib/common/events.js` | ソース | イベントエミッター |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | 設定 | DBスキーマ定義 |
