# 通知設計書 24-マイルストーンSlack通知

## 概要

本ドキュメントは、Ghost CMSにおけるマイルストーン達成時のSlack通知機能の設計仕様を記載する。

### 本通知の処理概要

ARR（年間経常収益）または会員数のマイルストーンが達成された際に、設定されたSlack Webhookに通知を送信する機能である。Ghost(Pro)などのホスティングサービスで使用され、サイト運営者の成長を祝福し、モチベーション向上に貢献する。

**業務上の目的・背景**：サブスクリプションビジネスにおいて、ARRや会員数の節目（マイルストーン）を達成することは重要なビジネス指標である。このマイルストーン達成をSlackで通知することで、運営チームがリアルタイムで成長を実感でき、チームの士気向上やビジネス戦略の検討に役立てることができる。主にGhost(Pro)のホスティング環境で利用される機能である。

**通知の送信タイミング**：`MilestoneCreatedEvent`ドメインイベントが発火した際に通知が送信される。ただし、マイルストーンの`meta.reason`が`skipped`または`initial`の場合は送信されない。

**通知の受信者**：Ghost(Pro)の管理用Slack Webhookに紐づくSlackチャンネル。`hostSettings.milestones.url`で設定されたWebhook URLに送信される。

**通知内容の概要**：達成したマイルストーンの種類（ARR/Members）、達成値、現在値、メール送信状態（送信済み日時または未送信理由）が含まれる。

**期待されるアクション**：運営チームはマイルストーン達成を確認し、顧客への祝福メッセージ送信、マーケティング施策の検討、サポート対応の優先度調整などを行う。

## 通知種別

Slack通知（Incoming Webhook / Block Kit形式）

## 送信仕様

### 基本情報

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

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

1. `hostSettings.milestones.enabled`が`true`かつ`hostSettings.milestones.url`が設定されている場合のみ有効
2. `minThreshold`設定値よりマイルストーン値が大きい場合のみ送信
3. 送信先URLはホスト設定（`hostSettings.milestones.url`）から取得

## 通知テンプレート

### Slack通知の場合

| 項目 | 内容 |
|-----|------|
| ユーザー名 | `Ghost Milestone Service` |
| unfurl_links | `false` |
| 形式 | Block Kit（attachments内blocks） |

### メッセージ構造（ARRマイルストーンの例）

```json
{
    "unfurl_links": false,
    "username": "Ghost Milestone Service",
    "attachments": [
        {
            "color": "#36a64f",
            "blocks": [
                {
                    "type": "header",
                    "text": {
                        "type": "plain_text",
                        "text": ":tada: ARR Milestone $1,000 reached!",
                        "emoji": true
                    }
                },
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": "New *ARR Milestone* achieved for <https://example.com|https://example.com>"
                    }
                },
                {
                    "type": "divider"
                },
                {
                    "type": "section",
                    "fields": [
                        {
                            "type": "mrkdwn",
                            "text": "*Milestone:*\n$1,000"
                        },
                        {
                            "type": "mrkdwn",
                            "text": "*Current ARR:*\n$1,234"
                        }
                    ]
                },
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": "*Email sent:*\n15 Jan 2024"
                    }
                }
            ]
        }
    ]
}
```

### 添付ファイル

なし

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| milestone.type | マイルストーンタイプ（arr/members） | MilestoneCreatedEvent | Yes |
| milestone.value | マイルストーン値 | MilestoneCreatedEvent | Yes |
| milestone.currency | 通貨コード（ARRの場合） | MilestoneCreatedEvent | No |
| milestone.emailSentAt | メール送信日時 | MilestoneCreatedEvent | No |
| meta.reason | 通知理由 | MilestoneCreatedEvent | No |
| meta.currentValue | 現在値 | MilestoneCreatedEvent | No |
| siteUrl | サイトURL | hostSettings経由 | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| ドメインイベント | `MilestoneCreatedEvent` | 設定有効かつminThreshold超過 | マイルストーン作成時に自動送信 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 機能無効 | `hostSettings.milestones.enabled`が`false`の場合 |
| URL未設定 | `hostSettings.milestones.url`が未設定の場合 |
| 閾値未達 | マイルストーン値が`minThreshold`以下の場合 |
| スキップ理由 | `meta.reason`が`skipped`の場合 |
| 初期理由 | `meta.reason`が`initial`の場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[MilestoneCreatedEvent発火] --> B{isEnabled?}
    B -->|false| C[終了]
    B -->|true| D{webhookUrl設定?}
    D -->|なし| C
    D -->|あり| E{reason確認}
    E -->|skipped| C
    E -->|initial| C
    E -->|その他| F{minThreshold確認}
    F -->|未達| C
    F -->|超過| G[notifyMilestoneReceived呼出]
    G --> H[メッセージ構築]
    H --> I[send関数呼出]
    I --> J{URL検証}
    J -->|不正| K[エラーログ]
    J -->|正常| L[HTTP POST送信]
    L --> M{送信結果}
    M -->|成功| N[終了]
    M -->|失敗| O[リトライ]
    K --> C
```

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

### 参照テーブル一覧

データベースへの直接参照はなし。ドメインイベントのペイロードから全ての情報を取得する。

| データソース | 用途 | 備考 |
|-------------|------|------|
| MilestoneCreatedEvent | マイルストーン情報 | ドメインイベントペイロード |
| hostSettings | Webhook URL・設定 | config経由 |
| urlUtils | サイトURL | 設定から取得 |

### 更新テーブル一覧

なし（通知送信のみ）

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| URL不正エラー | Webhook URLが空または不正な形式 | エラーログ出力、送信スキップ |
| HTTPリクエスト失敗 | ネットワークエラー、タイムアウト | リトライ（最大5回） |
| イベント処理エラー | notifyMilestoneReceived内でエラー | エラーログ出力、処理継続 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 5回（本番）、0回（テスト環境） |
| リトライ間隔 | gotライブラリのデフォルト |
| リトライ対象エラー | HTTPエラー |

## 配信設定

### レート制限

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

### 配信時間帯

制限なし（マイルストーン達成時に即時送信）

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

- Webhook URLは`hostSettings`（Ghost(Pro)の設定）から取得され、管理画面からは変更不可
- URL検証に`@tryghost/validator`のisURLを使用
- User-Agentヘッダーに`Ghost/{version}`を設定し、送信元を明示

## 備考

- この機能は主にGhost(Pro)環境向けであり、セルフホスト環境では`hostSettings`が設定されていないため動作しない
- `minThreshold`設定により、小さなマイルストーンの通知を抑制可能
- 金額表示は`Intl.NumberFormat`を使用して通貨フォーマットされる
- 日付表示は`moment`を使用して`D MMM YYYY`形式でフォーマットされる

---

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

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

### 推奨読解順序

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

MilestoneCreatedEventとSlackメッセージの構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | slack-notifications-service.js | `ghost/core/core/server/services/slack-notifications/slack-notifications-service.js` | 7-31行目、型定義とインターフェース |
| 1-2 | slack-notifications.js | `ghost/core/core/server/services/slack-notifications/slack-notifications.js` | 107-145行目、blocks構造 |

**読解のコツ**: Slack Block Kit APIの仕様を踏まえ、`header`, `section`, `divider`の各ブロックタイプの意味を把握する。

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

サービス初期化とイベント購読を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | service.js | `ghost/core/core/server/services/slack-notifications/service.js` | サービスラッパーと初期化 |

**主要処理フロー**:
1. **41-57行目**: `init()`でサービス初期化と設定取得
2. **19-39行目**: `create()`でSlackNotificationsServiceインスタンス生成

#### Step 3: イベント処理ロジックを理解する

ドメインイベントの購読と処理フローを追う。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | slack-notifications-service.js | `ghost/core/core/server/services/slack-notifications/slack-notifications-service.js` | SlackNotificationsServiceクラス |

**主要処理フロー**:
- **69-83行目**: `#handleEvent()`メソッド - 条件チェックと通知実行
- **85-89行目**: `subscribeEvents()` - イベント購読

#### Step 4: 通知構築・送信ロジックを理解する

Slackメッセージの構築と送信処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | slack-notifications.js | `ghost/core/core/server/services/slack-notifications/slack-notifications.js` | SlackNotificationsクラス |

**主要処理フロー**:
- **57-148行目**: `notifyMilestoneReceived()` - メッセージ構築
- **157-180行目**: `send()` - HTTP送信
- **189-199行目**: `#getFormattedAmount()` - 金額フォーマット
- **206-208行目**: `#getFormattedDate()` - 日付フォーマット

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

```
Ghost起動時
    │
    └─ SlackNotificationsServiceWrapper.init() [service.js:41-57]
           │
           ├─ hostSettings取得
           │
           ├─ SlackNotificationsServiceWrapper.create() [19-39]
           │      │
           │      ├─ new SlackNotifications({webhookUrl, siteUrl, logging})
           │      │
           │      └─ new SlackNotificationsService({...})
           │
           └─ subscribeEvents() [slack-notifications-service.js:85-89]
                  │
                  └─ DomainEvents.subscribe(MilestoneCreatedEvent, ...)

MilestoneCreatedEvent発火時
    │
    └─ #handleEvent(MilestoneCreatedEvent, event) [69-83]
           │
           ├─ [条件チェック]
           │      ├─ isEnabled?
           │      ├─ webhookUrl?
           │      └─ minThreshold < milestone.value?
           │
           └─ slackNotifications.notifyMilestoneReceived(event.data) [57-148]
                  │
                  ├─ [reason確認] skipped/initial → return
                  │
                  ├─ #getFormattedAmount() [189-199]
                  │
                  ├─ #getFormattedDate() [206-208]
                  │
                  ├─ blocksオブジェクト構築 [107-134]
                  │
                  ├─ slackData構築 [136-145]
                  │
                  └─ send(slackData, webhookUrl) [157-180]
                         │
                         ├─ validator.isURL(url)
                         │
                         └─ got.post(url, requestOptions)
```

### データフロー図

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

MilestoneCreatedEvent
    │
    ├─ milestone ─────────▶ SlackNotificationsService
    │   ├─ type             #handleEvent()
    │   ├─ value                  │
    │   ├─ currency               ▼
    │   └─ emailSentAt      条件チェック
    │                             │
    └─ meta ─────────────▶       │
        ├─ reason                 │
        └─ currentValue           │
                                  ▼
hostSettings ────────────▶ notifyMilestoneReceived()
    ├─ milestones.enabled         │
    ├─ milestones.url             ▼
    └─ milestones.minThreshold  金額/日付フォーマット
                                  │
urlUtils.getSiteUrl() ───▶       │
                                  ▼
                           blocksオブジェクト構築
                                  │
                                  ▼
                           slackData構築
                                  │
                                  ▼
                           send() ──────────────────▶ Slack Webhook API
                                  │
                           [エラー時]
                                  ▼
                           logging.error() ─────────▶ ログファイル
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| slack-notifications.js | `ghost/core/core/server/services/slack-notifications/slack-notifications.js` | ソース | Slackメッセージ構築と送信 |
| slack-notifications-service.js | `ghost/core/core/server/services/slack-notifications/slack-notifications-service.js` | ソース | イベント購読と処理制御 |
| service.js | `ghost/core/core/server/services/slack-notifications/service.js` | ソース | サービス初期化ラッパー |
| milestone-created-event.js | `ghost/core/core/server/services/milestones/milestone-created-event.js` | ソース | ドメインイベント定義 |
