# 機能設計書 8-メンバー登録

## 概要

本ドキュメントは、Ghost CMSにおけるメンバー登録機能について、その設計仕様を記載する。メンバー登録機能は、サイト購読者（メンバー）の新規登録を担う。

### 本機能の処理概要

メンバー登録機能は、Ghost CMSサイトの購読者（メンバー）を管理する機能である。メンバーはメールアドレスを基にした識別子を持ち、無料メンバー、有料メンバー（Stripe連携）、招待メンバー（Complimentary）の3種類のステータスを持つ。メンバーにはラベル（タグ）、購読ニュースレター、所属ティア（製品）などの属性を設定できる。

**業務上の目的・背景**：パブリッシャーがサイト購読者を管理し、ニュースレター配信や有料コンテンツの提供を行うためにメンバー管理機能が必要。メンバー登録はその基盤となる機能であり、自己登録（Portal経由）と管理者による手動登録（Admin API経由）の両方をサポートする。

**機能の利用シーン**：
- 読者がPortal経由で無料メンバーとして登録する
- 管理者が管理画面からメンバーを手動追加する
- CSVファイルからメンバーを一括インポートする
- Stripe経由で有料メンバーとして登録する

**主要な処理内容**：
1. メンバーの新規登録（POST /ghost/api/admin/members/）
2. メンバー一覧の取得（GET /ghost/api/admin/members/）
3. メンバーの編集・更新（PUT /ghost/api/admin/members/:id/）
4. メンバーの削除（DELETE /ghost/api/admin/members/:id/）
5. メンバーの一括削除（DELETE /ghost/api/admin/members/）
6. メンバーのCSVインポート（POST /ghost/api/admin/members/upload/）
7. メンバーのCSVエクスポート（GET /ghost/api/admin/members/export/）

**関連システム・外部連携**：
- Stripe（有料メンバー・サブスクリプション管理）
- メールサービス（ウェルカムメール・マジックリンク送信）
- Gravatar（アバター画像取得）

**権限による制御**：
- Owner/Administrator: 全メンバー操作が可能
- Editor/Author/Contributor: メンバー情報の閲覧のみ（browseアクションのみ）

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 10 | メンバー一覧画面 | 主機能 | メンバーの一覧表示・検索・フィルタ |
| 11 | メンバー詳細画面 | 主機能 | メンバー情報の表示・編集 |
| 15 | Portalウィジェット | 補助機能 | メンバー自己登録 |

## 機能種別

CRUD操作

## 入力仕様

### 入力パラメータ

#### メンバー登録（add）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| email | string | Yes | メールアドレス | isEmail, maxlength: 191, unique |
| name | string | No | 氏名 | maxlength: 191 |
| note | string | No | 管理者メモ | maxlength: 2000 |
| expertise | string | No | 専門分野 | maxlength: 50 |
| labels | array | No | ラベル配列 | - |
| newsletters | array | No | 購読ニュースレター配列 | - |
| products | array | No | 所属ティア（製品）配列 | - |
| comped | boolean | No | 招待メンバーフラグ | Stripe接続必須 |
| stripe_customer_id | string | No | StripeカスタマーID | Stripe接続必須 |

#### メンバー編集（edit）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | string | Yes | メンバーID | - |
| email | string | No | メールアドレス | isEmail, maxlength: 191, unique |
| name | string | No | 氏名 | maxlength: 191 |
| note | string | No | 管理者メモ | maxlength: 2000 |
| labels | array | No | ラベル配列 | - |
| newsletters | array | No | 購読ニュースレター配列 | - |
| products | array | No | 所属ティア（製品）配列 | - |

### 入力データソース

- 管理画面（Ghost Admin）のメンバー管理画面
- Portal（フロントエンド登録フォーム）
- Admin APIを通じた外部クライアントからのリクエスト
- CSVファイルインポート

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | メンバーの一意識別子 |
| uuid | string | UUID（36文字） |
| email | string | メールアドレス |
| name | string | 氏名 |
| note | string | 管理者メモ |
| expertise | string | 専門分野 |
| status | string | ステータス（free/paid/comped） |
| labels | array | ラベル配列 |
| newsletters | array | 購読ニュースレター配列 |
| products | array | 所属ティア配列 |
| subscriptions | array | サブスクリプション配列 |
| avatar_image | string | Gravatarアバター画像URL |
| email_count | integer | 送信メール数 |
| email_opened_count | integer | 開封メール数 |
| email_open_rate | integer | 開封率（%） |
| email_suppression | object | メール抑制情報 |
| geolocation | string | 位置情報 |
| last_seen_at | datetime | 最終アクセス日時 |
| created_at | datetime | 登録日時 |
| updated_at | datetime | 更新日時 |

### 出力先

- APIレスポンス（JSON形式）
- データベース（membersテーブル）
- CSVエクスポート

## 処理フロー

### 処理シーケンス

```
1. APIリクエスト受信
   └─ リクエストパラメータのバリデーション
2. 権限チェック
   └─ ユーザーロールの確認
3. メールアドレス重複チェック
   └─ 既存メンバーとの重複確認
4. Stripe連携（該当時）
   ├─ Stripe接続確認
   ├─ Stripeカスタマーリンク
   └─ サブスクリプション設定
5. メンバー作成/更新
   ├─ メンバーレコード作成
   ├─ ラベル関連付け
   ├─ ニュースレター関連付け
   └─ ティア関連付け
6. ウェルカムメール送信（該当時）
   └─ マジックリンクメール送信
7. レスポンス返却
   └─ メンバーデータをJSON形式で返却
```

### フローチャート

```mermaid
flowchart TD
    A[APIリクエスト受信] --> B{権限チェック}
    B -->|権限なし| C[403エラー]
    B -->|権限あり| D{メール重複チェック}
    D -->|重複あり| E[400 ValidationError]
    D -->|重複なし| F{Stripe連携?}
    F -->|Yes| G{Stripe接続済?}
    G -->|No| H[400 ValidationError]
    G -->|Yes| I[Stripeカスタマーリンク]
    I --> J[メンバー作成]
    F -->|No| J
    J --> K[ラベル・ニュースレター関連付け]
    K --> L{ウェルカムメール?}
    L -->|Yes| M[マジックリンク送信]
    L -->|No| N[レスポンス返却]
    M --> N
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | メールアドレス一意制約 | メールアドレスはシステム全体で一意である必要がある | メンバー作成・更新時 |
| BR-002 | ステータス自動設定 | Stripe課金中はpaid、招待中はcomped、それ以外はfree | メンバー作成・更新時 |
| BR-003 | UUID自動生成 | メンバー作成時にUUIDとtransient_idを自動生成 | メンバー作成時 |
| BR-004 | Stripe必須チェック | comped=trueまたはstripe_customer_id指定時はStripe接続必須 | メンバー作成時 |
| BR-005 | ラベル重複排除 | 同一名（大文字小文字無視）のラベルは重複登録しない | メンバー保存時 |
| BR-006 | 一覧取得制限 | 一覧取得時の最大件数は100件 | browse時 |
| BR-007 | メール検証抑制 | メール検証が必要な状態ではウェルカムメールを送信しない | メンバー作成時 |

### 計算ロジック

- **開封率（email_open_rate）**: email_opened_count / email_count * 100
- **Gravatarアバター**: MD5(email) を使用してGravatar URLを生成（useGravatarがtrueの場合）

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| メンバー作成 | members | INSERT | 新規メンバーレコードの挿入 |
| メンバー更新 | members | UPDATE | メンバーデータの更新 |
| メンバー削除 | members | DELETE | メンバーレコードの削除 |
| ラベル関連付け | members_labels | INSERT/DELETE | ラベルとの関連付け |
| ニュースレター関連付け | members_newsletters | INSERT/DELETE | ニュースレターとの関連付け |
| ティア関連付け | members_products | INSERT/DELETE | ティアとの関連付け |

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

#### members

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | 自動生成 | 24文字の文字列 |
| INSERT | uuid | 自動生成 | crypto.randomUUID() |
| INSERT | transient_id | 自動生成 | crypto.randomUUID() |
| INSERT | email | リクエスト値 | 必須、一意制約 |
| INSERT | name | リクエスト値 | オプション |
| INSERT | status | 'free' | デフォルト値 |
| INSERT | email_count | 0 | デフォルト値 |
| INSERT | email_opened_count | 0 | デフォルト値 |
| INSERT | enable_comment_notifications | true | デフォルト値 |
| UPDATE | updated_at | 現在日時 | 自動更新 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 400 | ValidationError | メールアドレスが既に存在 | 別のメールアドレスを使用 |
| 400 | ValidationError | Stripe未接続でcomped/stripe_customer_id指定 | Stripe接続後に実行 |
| 403 | NoPermissionError | 権限不足 | 適切な権限を持つユーザーで実行 |
| 404 | NotFoundError | メンバーが存在しない | 正しいIDを指定 |

### リトライ仕様

特別なリトライ処理は実装されていない。一意制約違反の場合は明確なエラーメッセージを返す。

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

メンバーの作成・編集・削除はトランザクション内で実行される。Stripeカスタマーリンク失敗時はメンバーレコードをロールバック削除する。

## パフォーマンス要件

- メンバー一覧取得: 最大100件制限（limit > 100 の場合は100に制限）
- メンバー検索: name, emailでのLIKE検索をサポート

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

- **認証**: Admin API認証（Sessionまたは Admin API Key）が必要
- **認可**: 権限チェック（permissions: true）が設定されている
- **メール検証**: 不正登録防止のためメール検証が必要な場合はウェルカムメール抑制
- **Gravatar**: プライバシー設定でGravatar使用を無効化可能（isPrivacyDisabled）
- **監査ログ**: メンバーの作成・編集・削除はactionsテーブルに記録（actionsCollectCRUD: true）

## 備考

- email_recipientsリレーションはメンバー削除時に保持（destroyRelated: false）- 分析・履歴目的
- transient_idはログアウト時にサイクル（更新）される
- ティア（products）の有効期限はmembers_products.expiry_atで管理

---

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

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

### 推奨読解順序

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

メンバーのモデル定義を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | member.js | `ghost/core/core/server/models/member.js` | Memberモデルの全体構造 |
| 1-2 | schema.js | `ghost/core/core/server/data/schema/schema.js` | membersテーブル定義（417-444行目） |

**member.js の主要処理フロー**:
- **7行目**: テーブル名 'members'
- **9-11行目**: actionsCollectCRUD: true（監査ログ記録）
- **13-22行目**: defaults() - デフォルト値（status: 'free', uuid, transient_id, email_count: 0等）
- **24-65行目**: filterExpansions() - フィルタ展開定義（label, products, newsletters等）
- **67-139行目**: filterRelations() - フィルタリレーション定義
- **141行目**: relationships配列（products, labels, stripeCustomers, email_recipients, newsletters）
- **145-155行目**: relationshipConfig - リレーション設定（email_recipientsはdestroyRelated: false）
- **171-180行目**: products() - belongsToMany関係（expiry_at付き）
- **182-190行目**: newsletters() - belongsToMany関係
- **197-206行目**: labels() - belongsToMany関係
- **279-324行目**: onSaving() - ラベル重複排除処理
- **379-384行目**: searchQuery() - 検索クエリ（name, emailでLIKE検索）
- **394-407行目**: toJSON() - Gravatarアバター生成
- **424-437行目**: add() - トランザクション付き作成処理
- **439-452行目**: edit() - トランザクション付き編集処理
- **454-461行目**: destroy() - トランザクション付き削除処理

**schema.js のmembers定義（417-444行目）**:
- id, uuid, transient_id, email（unique）, status, name, expertise, note
- enable_comment_notifications, email_count, email_opened_count, email_open_rate
- email_disabled, last_seen_at, last_commented_at, commenting

#### Step 2: APIエンドポイントを理解する

メンバーAPIのエンドポイント定義を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | members.js | `ghost/core/core/server/api/endpoints/members.js` | Admin API エンドポイント |

**主要処理フロー**:
- **32行目**: allowedIncludes = ['email_recipients', 'products', 'tiers']
- **38-65行目**: browseアクション - memberBREADService.browse()使用
- **67-97行目**: readアクション - memberBREADService.read()使用
- **99-128行目**: addアクション - 検証必須時ウェルカムメール抑制、email_typeバリデーション
- **130-151行目**: editアクション
- **153-176行目**: logoutアクション - transient_idのサイクル
- **178-238行目**: editSubscriptionアクション - サブスクリプション更新/キャンセル
- **240-282行目**: createSubscriptionアクション - Stripeサブスクリプション作成
- **284-308行目**: destroyアクション - cancelStripeSubscriptionsオプション
- **310-338行目**: bulkDestroyアクション - 一括削除
- **340-368行目**: bulkEditアクション - 一括編集（unsubscribe, addLabel, removeLabel）
- **370-400行目**: exportCSVアクション - CSVエクスポート
- **402-445行目**: importCSVアクション - CSVインポート（ラベル自動付与）

#### Step 3: メンバーサービスを理解する

メンバー管理のビジネスロジックを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | member-bread-service.js | `ghost/core/core/server/services/members/members-api/services/member-bread-service.js` | BREAD操作サービス |

**主要処理フロー**:
- **69-133行目**: attachSubscriptionsToMember() - Complimentaryサブスクリプション追加
- **220-279行目**: read() - メンバー詳細取得（withRelated展開、email_suppression、unsubscribe_url付与）
- **281-349行目**: add() - メンバー作成（Stripe連携、ウェルカムメール送信）
- **351-395行目**: edit() - メンバー編集（email_disabled更新、comped設定）
- **451-453行目**: logout() - transient_idサイクル
- **455-526行目**: browse() - 一覧取得（limit制限100、バルクサプレッションデータ取得）

**重要ポイント**:
- add()でStripe未接続時にcomped/stripe_customer_id指定するとエラー（282-289行目）
- add()でメール重複時はValidationError（301-307行目）
- browse()でlimit > 100の場合は100に制限（467-469行目）

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

```
API Layer (members.js)
    │
    ├─ browse(frame)
    │      └─ memberBREADService.browse()
    │             ├─ memberRepository.list()
    │             ├─ fetchSubscriptionOffers()
    │             ├─ attachSubscriptionsToMember()
    │             ├─ attachOffersToSubscriptions()
    │             └─ emailSuppressionList.getBulkSuppressionData()
    │
    ├─ read(frame)
    │      └─ memberBREADService.read()
    │             ├─ memberRepository.get()
    │             ├─ attachSubscriptionsToMember()
    │             ├─ attachAttributionsToMember()
    │             └─ emailSuppressionList.getSuppressionData()
    │
    ├─ add(frame)
    │      ├─ verificationTrigger.checkVerificationRequired()
    │      └─ memberBREADService.add()
    │             ├─ memberAttributionService.getAttributionFromContext()
    │             ├─ memberRepository.create()
    │             ├─ memberRepository.linkStripeCustomer()
    │             ├─ emailService.sendEmailWithMagicLink()
    │             └─ memberRepository.setComplimentarySubscription()
    │
    ├─ edit(frame)
    │      └─ memberBREADService.edit()
    │             ├─ emailSuppressionList.getSuppressionData()
    │             ├─ memberRepository.update()
    │             ├─ memberRepository.setComplimentarySubscription()
    │             └─ memberRepository.cancelComplimentarySubscription()
    │
    └─ destroy(frame)
           └─ membersService.api.members.destroy()

Model Layer (member.js)
    │
    ├─ Member.add() - トランザクション付き作成
    ├─ Member.edit() - トランザクション付き編集
    ├─ Member.destroy() - トランザクション付き削除
    │
    └─ onSaving()
           └─ ラベル重複排除処理
```

### データフロー図

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

Admin UI / Portal / API
       │
       ▼
┌─────────────────┐
│ POST /members/  │
│ PUT /members/   │
└────────┬────────┘
         │
         ▼
┌─────────────────┐    ┌─────────────────┐
│ Permission      │───▶│ Validation      │
│ Check           │    │ - email format  │
│                 │    │ - email unique  │
└─────────────────┘    └────────┬────────┘
                                │
                    ┌───────────┴───────────┐
                    │                       │
                    ▼                       ▼
           ┌─────────────────┐     ┌─────────────────┐
           │ Stripe連携      │     │ 通常登録        │
           │ - customer link │     │                 │
           │ - subscription  │     │                 │
           └────────┬────────┘     └────────┬────────┘
                    │                       │
                    └───────────┬───────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Member Model    │
                       │ - create/update │
                       │ - labels        │
                       │ - newsletters   │
                       │ - products      │
                       └────────┬────────┘
                                │
                    ┌───────────┴───────────┐
                    │                       │
                    ▼                       ▼
           ┌─────────────────┐     ┌─────────────────┐
           │ members         │     │ members_labels  │
           │ table           │     │ members_products│
           │                 │     │ members_news... │
           └────────┬────────┘     └────────┬────────┘
                    │                       │
                    └───────────┬───────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Email Service   │
                       │ (optional)      │
                       │ - welcome email │
                       │ - magic link    │
                       └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ JSON Response   │
                       │ + Gravatar      │
                       │ + suppression   │
                       └─────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| member.js | `ghost/core/core/server/models/member.js` | モデル | Memberモデル定義 |
| members.js | `ghost/core/core/server/api/endpoints/members.js` | API | Admin API エンドポイント |
| member-bread-service.js | `ghost/core/core/server/services/members/members-api/services/member-bread-service.js` | サービス | BREAD操作サービス |
| member-repository.js | `ghost/core/core/server/services/members/members-api/repositories/member-repository.js` | リポジトリ | データアクセス層 |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | スキーマ | membersテーブル定義（417-444行目） |
| index.js | `ghost/core/core/server/services/members/index.js` | サービス | メンバーサービスエントリポイント |
