# 機能設計書 64-マイルストーン通知

## 概要

本ドキュメントは、Ghost CMSにおけるマイルストーン通知機能の設計を記述する。この機能は、メンバー数やARR（年間経常収益）が特定の節目に達した際に、サイト運営者に通知を送信する機能である。

### 本機能の処理概要

この機能では、定期的にメンバー数とARRをチェックし、設定されたマイルストーン値に達した場合にメール通知やSlack通知を送信する。

**業務上の目的・背景**：サイト運営者にとって、メンバー数100名達成やARR $1,000達成などの節目は重要なビジネスマイルストーンである。これらの達成を自動検出し、祝福通知を送ることでモチベーション向上に貢献する。

**機能の利用シーン**：
- メンバー数が100, 1000, 10000などの節目に達した際
- ARRが$100, $1000, $10000などの節目に達した際
- マイルストーン達成のメール通知受信

**主要な処理内容**：
1. メンバー数の定期チェック
2. ARR（年間経常収益）の定期チェック
3. マイルストーン達成の検出
4. マイルストーンレコードの保存
5. メール通知の送信（条件付き）
6. MilestoneCreatedEventの発行

**関連システム・外部連携**：Stripe（ARR計算用）、メール配信サービス、Slack通知サービス

**権限による制御**：システム自動処理のため、権限による制御は適用されない

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | - | - | バックグラウンド処理のため関連画面なし |

## 機能種別

バッチ処理 / イベント通知

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| type | string | Yes | 'arr' または 'members' | 列挙値 |

### 入力データソース

- Members テーブル（メンバー数）
- Stripe Customer Subscription テーブル（ARR計算）
- Settings テーブル（デフォルト通貨）
- Config（milestonesConfig）

## 出力仕様

### 出力データ（Milestone）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | ObjectID | マイルストーンID |
| type | string | 'arr' または 'members' |
| value | number | 達成値 |
| currency | string | 通貨（ARRのみ） |
| createdAt | Date | 作成日時 |
| emailSentAt | Date | メール送信日時（送信した場合） |

### 出力先

- Milestones テーブル
- DomainEvents（MilestoneCreatedEvent）
- メール通知

## 処理フロー

### 処理シーケンス

```
1. チェック開始
   └─ checkMilestones(type) を呼び出し

2. 現在値の取得
   ├─ members: getMembersCount()
   └─ arr: getARR() + getDefaultCurrency()

3. 達成マイルストーンの特定
   └─ 設定値と現在値を比較し、達成したマイルストーンを抽出

4. 各マイルストーンの処理
   ├─ 既存チェック（重複防止）
   ├─ 初回実行判定
   ├─ スキップ判定（既に高いマイルストーン達成済み）
   └─ 保存処理

5. メール送信判定
   ├─ 最後のメール送信から14日以上経過
   └─ 直近7日間でメンバーインポートなし

6. イベント発行
   └─ MilestoneCreatedEvent
```

### フローチャート

```mermaid
flowchart TD
    A[checkMilestones開始] --> B{type?}
    B -->|arr| C[ARR取得]
    B -->|members| D[メンバー数取得]
    C --> E[通貨設定確認]
    D --> F[達成マイルストーン特定]
    E --> F
    F --> G{マイルストーンあり?}
    G -->|No| H[終了]
    G -->|Yes| I[各マイルストーン処理]
    I --> J{既存チェック}
    J -->|存在| I
    J -->|なし| K{初回実行?}
    K -->|Yes| L[メールなしで保存]
    K -->|No| M{スキップ対象?}
    M -->|Yes| L
    M -->|No| N[メール送信判定]
    N --> O{送信可能?}
    O -->|Yes| P[メール付きで保存]
    O -->|No| Q[理由付きで保存]
    P --> I
    Q --> I
    L --> I
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-01 | 初回実行除外 | 初回実行時はメール送信しない（reason: initial） | 既存マイルストーンがない場合 |
| BR-02 | スキップ処理 | 既に高いマイルストーンがある場合はメールなし（reason: skipped） | 最高値より低いマイルストーン |
| BR-03 | メール間隔制限 | 最後のメール送信から一定日数（minDaysSinceLastEmail）以上必要 | メール送信判定時 |
| BR-04 | インポート除外 | 直近でメンバーインポートがあった場合はメール送信しない（reason: import） | メンバーインポート検出時 |
| BR-05 | Stripeライブ必須 | ARRマイルストーンはStripeライブキー使用時のみ | ARRチェック時 |
| BR-06 | 通貨フィルタ | サポートされている通貨のみ処理対象 | ARRチェック時 |

### 計算ロジック

ARR計算: Stripe Subscriptionの月額合計 x 12

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| メンバー数取得 | members | SELECT | COUNT集計 |
| ARR取得 | stripe_customer_subscriptions | SELECT | 合計計算 |
| マイルストーン取得 | milestones | SELECT | 既存チェック |
| マイルストーン保存 | milestones | INSERT | 新規保存 |

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

#### milestones

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | type, value, currency | 既存チェック | getByARR, getByCount |
| SELECT | type, email_sent_at | 最新マイルストーン取得 | getLatestByType |
| INSERT | id, type, value, currency, created_at, email_sent_at | Milestone.create()から | save |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | ValidationError | 無効なtype指定 | members をデフォルトとして処理 |
| - | ValidationError | 無効なvalue | エラーを投げる |
| - | ValidationError | 無効な日付形式 | エラーを投げる |

### リトライ仕様

バッチ処理のため、失敗時は次回実行時に再試行

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

各マイルストーン保存は独立したトランザクション

## パフォーマンス要件

- 起動時に0-4日のランダム遅延で実行（サービス負荷分散）
- 開発環境では5秒後に実行

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

- Stripeキーの種別（ライブ/テスト）をチェック
- 内部処理のためAPI経由のアクセスは不可

## 備考

- マイルストーン設定はconfig.jsのmilestonesConfigで定義
- MilestoneCreatedEventはSlack通知サービスも購読

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | milestone.js | `ghost/core/core/server/services/milestones/milestone.js` | Milestoneエンティティの構造 |
| 1-2 | milestone.js | `ghost/core/core/server/models/milestone.js` | Bookshelfモデル定義 |

**読解のコツ**: ドメインエンティティ（services/milestone.js）とBookshelfモデル（models/milestone.js）を区別する。

**主要処理フロー（milestone.js サービス）**:
- **5-69行目**: Milestoneクラス定義、プライベートフィールド、getter
- **85-138行目**: create静的メソッド、バリデーション、イベント発行

#### Step 2: サービス初期化を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | service.js | `ghost/core/core/server/services/milestones/service.js` | サービス初期化とスケジューリング |

**主要処理フロー**:
- **8-23行目**: getStripeLiveEnabled - Stripeキー判定
- **32-56行目**: init - 依存関係の初期化
- **62-75行目**: run - マイルストーンチェック実行
- **83-103行目**: scheduleRun - 遅延実行スケジューリング

#### Step 3: メインロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | milestones-service.js | `ghost/core/core/server/services/milestones/milestones-service.js` | マイルストーン検出ロジック |

**主要処理フロー**:
- **57-67行目**: getLatestArrMilestone, getLatestMembersCountMilestone
- **84-97行目**: checkMilestoneExists - 重複チェック
- **106-111行目**: createMilestone - 保存処理
- **120-124行目**: getMatchedMilestones - 達成判定
- **137-149行目**: saveMileStoneAndSendEmail - メール送信付き保存
- **168-191行目**: shouldSendEmail - メール送信判定
- **196-257行目**: runARRQueries - ARRマイルストーンチェック
- **262-313行目**: runMemberQueries - メンバーマイルストーンチェック

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

```
service.js
    │
    ├─ init()
    │      └─ MilestonesService作成
    │              ├─ BookshelfMilestoneRepository
    │              └─ MilestoneQueries
    │
    └─ run()
           │
           ├─ checkMilestones('members')
           │      └─ runMemberQueries()
           │
           └─ checkMilestones('arr')
                  └─ runARRQueries()

MilestonesService
    │
    ├─ runMemberQueries() / runARRQueries()
    │      ├─ queries.getMembersCount() / getARR()
    │      ├─ getMatchedMilestones()
    │      ├─ checkMilestoneExists()
    │      └─ saveMileStoneAndSendEmail() / saveMileStoneWithoutEmail()
    │
    └─ createMilestone()
           └─ Milestone.create()
                  └─ MilestoneCreatedEvent発行
```

### データフロー図

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

Members DB ────────▶ getMembersCount() ────▶ 現在のメンバー数
                            │
Stripe DB ─────────▶ getARR() ──────────────▶ 現在のARR
                            │
                            ▼
Config ────────────▶ getMatchedMilestones() ──▶ 達成マイルストーン
(milestonesConfig)          │
                            ▼
                    checkMilestoneExists()
                            │
                            ▼
                    shouldSendEmail()
                            │
                            ▼
                    createMilestone() ──────▶ Milestones DB
                            │
                            ▼
                    MilestoneCreatedEvent ──▶ Slack通知
                                              メール通知
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| service.js | `ghost/core/core/server/services/milestones/service.js` | ソース | サービス初期化・実行 |
| milestones-service.js | `ghost/core/core/server/services/milestones/milestones-service.js` | ソース | メインロジック |
| milestone.js | `ghost/core/core/server/services/milestones/milestone.js` | ソース | ドメインエンティティ |
| milestone-created-event.js | `ghost/core/core/server/services/milestones/milestone-created-event.js` | ソース | イベント定義 |
| milestone-queries.js | `ghost/core/core/server/services/milestones/milestone-queries.js` | ソース | DBクエリ |
| bookshelf-milestone-repository.js | `ghost/core/core/server/services/milestones/bookshelf-milestone-repository.js` | ソース | リポジトリ |
| milestone.js | `ghost/core/core/server/models/milestone.js` | ソース | Bookshelfモデル |
| index.js | `ghost/core/core/server/services/milestones/index.js` | ソース | エクスポート |
