# 通知設計書 11-無料会員サインアップ通知

## 概要

本ドキュメントは、Ghost CMSにおける無料会員サインアップ通知機能の設計仕様を記載したものである。

### 本通知の処理概要

無料会員サインアップ通知は、サイトに新規無料会員が登録された際に、スタッフメンバーに対してメール通知を送信する機能である。この通知により、サイト運営者はリアルタイムで新規会員登録を把握し、会員獲得状況をモニタリングすることができる。

**業務上の目的・背景**：サイト運営者にとって、新規会員獲得は重要なビジネス指標である。無料会員の登録は、将来的な有料会員への転換の第一歩であり、サイトの成長を示す重要なシグナルとなる。この通知により、運営者は会員獲得の進捗をリアルタイムで把握し、マーケティング施策の効果測定やコンテンツ戦略の調整に活用できる。

**通知の送信タイミング**：新規無料会員が登録を完了した直後に、`MemberCreatedEvent`ドメインイベントが発火されたタイミングで送信される。具体的には、会員のステータスが`free`で、ソースが`api`または`member`の場合にトリガーされる。

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

**通知内容の概要**：新規会員の名前、メールアドレス、アトリビューション情報（リファラーソース、登録元ページ）、登録日時などが含まれる。サイトのロゴ、サイト名、アクセントカラーもメールに反映される。

**期待されるアクション**：受信者は「View member」ボタンまたはリンクから管理画面の会員詳細ページに直接アクセスし、新規会員の情報を確認・管理できる。また、メール末尾のリンクから通知設定を変更することも可能。

## 通知種別

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

## 送信仕様

### 基本情報

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

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

`models.User.getEmailAlertUsers('free-signup')` メソッドにより、以下の条件を満たすユーザーを取得：
- `status:active` - アクティブなユーザーのみ
- `free_member_signup_notification:true` - 無料会員サインアップ通知が有効

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

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | サイト設定のデフォルトメールアドレス（`settingsHelpers.getDefaultEmail()`） |
| 送信元名称 | サイト名 |
| 件名 | `Free member signup: {会員名}` |
| 形式 | HTML/テキスト両対応 |

### 本文テンプレート

```html
<!-- HTMLテンプレート: new-free-signup.hbs -->
<h1>You have a new free member</h1>

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

{{#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>
```

```text
<!-- テキストテンプレート: new-free-signup.txt.js -->
Congratulations!

You have a new free member: "{{memberData.name}}"

---

Sent to {{toEmail}} from {{siteDomain}}.
If you would no longer like to receive these notifications you can adjust your settings at {{staffUrl}}.
```

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| memberData.name | 会員の名前（未設定時はメールアドレス） | member.name \|\| member.email | Yes |
| memberData.email | 会員のメールアドレス | member.email | Yes |
| memberData.showEmail | 名前が設定されている場合true | !!member.name | Yes |
| memberData.adminUrl | 管理画面の会員詳細URL | urlUtils.urlFor('admin') + '/members/{id}' | Yes |
| memberData.initials | 名前のイニシャル | extractInitials(name) | No |
| memberData.location | 位置情報（国） | member.geolocation | No |
| memberData.createdAt | 登録日（フォーマット済み） | moment(member.created_at).format('D MMM YYYY') | No |
| referrerSource | リファラーソース | attribution.referrerSource | No |
| attributionTitle | 登録元ページタイトル | attribution.title | No |
| attributionUrl | 登録元ページURL | attribution.url | No |
| siteTitle | サイト名 | settingsCache.get('title') | Yes |
| siteIconUrl | サイトアイコンURL | blogIcon.getIconUrl() | No |
| siteUrl | サイトURL | urlUtils.getSiteUrl() | Yes |
| siteDomain | サイトドメイン | URLから抽出 | Yes |
| accentColor | アクセントカラー | settingsCache.get('accent_color') | No |
| fromEmail | 送信元メールアドレス | settingsHelpers.getDefaultEmail() | Yes |
| toEmail | 受信者メールアドレス | user.email | Yes |
| staffUrl | スタッフ設定URL | urlUtils.urlFor('admin') + '/settings/staff/{slug}/email-notifications' | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| ドメインイベント | MemberCreatedEvent | member.status === 'free' && source in ['api', 'member'] | 無料会員が新規登録された際に発火 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| ソースが'api'または'member'以外 | インポートなど別ソースからの会員作成では送信しない |
| 会員ステータスがfree以外 | 有料会員の場合は別の通知が送信される |
| 通知対象ユーザーが0人 | `free_member_signup_notification:true`のユーザーがいない場合 |

## 処理フロー

### 送信フロー

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

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| members | 新規会員情報取得 | id, name, email, geolocation, status, created_at |
| users | 通知対象スタッフ取得 | status:active, free_member_signup_notification:true |
| settings | サイト設定取得 | title, accent_color など |

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

#### members

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | 管理画面URL生成 | イベントから取得 |
| name | 会員名表示 | - |
| email | メールアドレス表示 | - |
| geolocation | 位置情報表示 | JSON形式で保存 |
| created_at | 登録日表示 | - |

#### users

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| email | 通知送信先 | status:active AND free_member_signup_notification:true |
| slug | スタッフ設定URL生成 | - |

### 更新テーブル一覧

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

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | SMTP接続エラー等 | logging.errorでログ出力、処理継続 |
| アトリビューション取得失敗 | memberAttributionServiceエラー | logging.warnでログ出力、attributionなしで送信継続 |
| ユーザー取得失敗 | DBエラー | エラーをスロー、上位でキャッチ |

### リトライ仕様

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

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし（GhostMailerに依存） |
| 1日あたり上限 | 制限なし |

### 配信時間帯

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

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

- 会員のメールアドレス情報が含まれるため、送信先はスタッフメンバーのみに限定
- 管理画面へのURLはBase64エンコードなどの難読化なしで送信（管理画面自体が認証必須）
- 個人の位置情報（国レベル）が含まれる可能性があるが、スタッフ向け通知のため許容

## 備考

- アトリビューション情報は`memberAttributionService`を通じて取得され、リファラーソースや登録元ページの情報を提供
- `homepage`というアトリビューションタイトルは表示時に`Homepage`に変換される
- 開発環境（NODE_ENV !== 'production'）ではメール本文がログに出力される

---

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

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

### 推奨読解順序

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

まず、通知に使用されるデータ構造を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | staff-service.js | `ghost/core/core/server/services/staff/staff-service.js` | 31-63行目: `getSerializedData`メソッドで会員・サブスクリプションデータの構造を理解 |
| 1-2 | user.js | `ghost/core/core/server/models/user.js` | 62-76行目: `defaults`でユーザーの通知設定デフォルト値を確認 |

**読解のコツ**: `getSerializedData`はmember, tier, subscription, offerの各オブジェクトを正規化している。通知に必要なデータ項目はここで定義される。

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

処理の起点となるイベントサブスクリプションを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | staff-service.js | `ghost/core/core/server/services/staff/staff-service.js` | 140-148行目: `subscribeEvents`内の`MemberCreatedEvent`サブスクリプション |

**主要処理フロー**:
1. **140-148行目**: `MemberCreatedEvent`をサブスクライブ
2. **143行目**: `handleEvent`を呼び出し
3. **97-111行目**: 無料会員の場合の処理分岐

#### Step 3: 通知対象ユーザー取得ロジック

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | user.js | `ghost/core/core/server/models/user.js` | 497-520行目: `getEmailAlertUsers`メソッドのフィルター条件 |

**主要処理フロー**:
- **501-502行目**: `free-signup`タイプの場合、`free_member_signup_notification:true`フィルターを追加

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | staff-service-emails.js | `ghost/core/core/server/services/staff/staff-service-emails.js` | 23-66行目: `notifyFreeMemberSignup`メソッド |
| 4-2 | new-free-signup.hbs | `ghost/core/core/server/services/staff/email-templates/new-free-signup.hbs` | HTMLテンプレート全体 |
| 4-3 | new-free-signup.txt.js | `ghost/core/core/server/services/staff/email-templates/new-free-signup.txt.js` | テキストテンプレート全体 |

**主要処理フロー**:
- **26行目**: 通知対象ユーザーを取得
- **32行目**: 件名を生成（絵文字付き）
- **42-55行目**: テンプレートデータを構築
- **57行目**: テンプレートをレンダリング
- **59-64行目**: メールを送信

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

```
MemberCreatedEvent（ドメインイベント発火）
    │
    └─ StaffService.handleEvent()
           │
           ├─ getDataFromIds() - 会員データ取得
           │      └─ models.Member.findOne()
           │
           ├─ memberAttributionService.getMemberCreatedAttribution() - アトリビューション取得
           │
           └─ StaffServiceEmails.notifyFreeMemberSignup()
                  │
                  ├─ models.User.getEmailAlertUsers('free-signup') - 通知対象取得
                  │
                  ├─ getMemberData() - 会員データ整形
                  │
                  ├─ renderEmailTemplate('new-free-signup') - テンプレートレンダリング
                  │      ├─ renderHTML() - Handlebarsでコンパイル
                  │      └─ renderText() - テキストテンプレート生成
                  │
                  └─ sendMail() - メール送信
                         └─ mailer.send()
```

### データフロー図

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

MemberCreatedEvent ───▶ StaffService ───▶ スタッフメンバーへのメール
  │                         │
  ├─ memberId              ├─ 会員データ取得
  ├─ source                ├─ アトリビューション取得
  └─ attribution           ├─ 通知対象ユーザー取得
                           ├─ テンプレートレンダリング
                           └─ メール送信

[参照データ]
  ├─ members テーブル
  ├─ users テーブル
  └─ settings テーブル
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| 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` | ソース | メール送信ロジック、テンプレート展開 |
| index.js | `ghost/core/core/server/services/staff/index.js` | ソース | StaffService初期化・DI |
| user.js | `ghost/core/core/server/models/user.js` | ソース | 通知対象ユーザー取得ロジック |
| new-free-signup.hbs | `ghost/core/core/server/services/staff/email-templates/new-free-signup.hbs` | テンプレート | HTMLメールテンプレート |
| new-free-signup.txt.js | `ghost/core/core/server/services/staff/email-templates/new-free-signup.txt.js` | テンプレート | テキストメールテンプレート |
| events.js | `ghost/core/core/shared/events.js` | ソース | MemberCreatedEventの定義 |
