# 通知設計書 15-ドネーション受領通知

## 概要

本ドキュメントは、Ghost CMSにおけるドネーション（一回払い）受領通知機能の設計仕様を記載したものである。

### 本通知の処理概要

ドネーション受領通知は、サイトが一回払いのドネーション（チップ/寄付）を受け取った際に、スタッフメンバーに対してメール通知を送信する機能である。この通知により、サイト運営者は支援者からの寄付をリアルタイムで把握し、感謝の意を伝えることができる。

**業務上の目的・背景**：ドネーション（チップ）機能は、サブスクリプション以外の収益源として機能する。読者がコンテンツを評価し、任意の金額で支援を行う際に送金される。ドネーションは一時的な収益であるが、読者のエンゲージメントを示す重要な指標であり、運営者にとって大きな励みとなる。メッセージ付きのドネーションに対しては、返信のアクションを促すことで読者との関係構築を支援する。

**通知の送信タイミング**：`DonationPaymentEvent`が発生した際に、`StaffServiceEmails.notifyDonationReceived`が呼び出されるタイミングで送信される。

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

**通知内容の概要**：ドネーション送信者の名前/メールアドレス、受領金額、ドネーションメッセージ（存在する場合）、会員情報へのリンク（会員の場合）が含まれる。

**期待されるアクション**：受信者は「Reply」または「Say thanks」ボタンから、ドネーション送信者にメールで感謝を伝えることが期待される。メッセージ付きの場合は返信、メッセージなしの場合は感謝のメールを送信するUIが提供される。

## 通知種別

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

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（DomainEventsサブスクライバー経由）、Promise.allSettledで並列送信 |
| 優先度 | 高 |
| リトライ | 個別のsendMailの失敗はログに記録、全体処理は継続 |

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

`models.User.getEmailAlertUsers('donation')` メソッドにより、以下の条件を満たすユーザーを取得：
- `status:active` - アクティブなユーザーのみ
- `donation_notifications:true` - ドネーション通知が有効（デフォルトはtrue）

該当する全ユーザーに対して並列でメールを送信する。

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | サイト設定のデフォルトメールアドレス |
| 送信元名称 | サイト名 |
| 件名 | `One-time payment received: {金額} from {名前}` |
| 形式 | HTML/テキスト両対応 |

### 本文テンプレート

```html
<!-- HTMLテンプレート: donation.hbs -->
<h1>Cha-ching! You received a tip.</h1>

<p>From: {{donation.name}} {{#if memberData}} - <a href="{{memberData.adminUrl}}">View</a>{{/if}}</p>
<p>Amount received: {{donation.amount}}</p>

{{#if donation.donationMessage}}
  <p>"{{donation.donationMessage}}"</p>
{{/if}}

{{#if donation.donationMessage}}
  <a href="mailto:{{donation.email}}">Reply</a>
{{else}}
  <a href="mailto:{{donation.email}}">Say thanks</a>
{{/if}}
```

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| donation.name | ドネーション送信者名 | donationPaymentEvent.name \|\| donationPaymentEvent.email | Yes |
| donation.email | ドネーション送信者メール | donationPaymentEvent.email | Yes |
| donation.amount | 金額（フォーマット済み） | getFormattedAmount(amount / 100, currency) | Yes |
| donation.donationMessage | ドネーションメッセージ | donationPaymentEvent.donationMessage | No |
| memberData | 会員データ（会員の場合） | getMemberData() | No |
| memberData.adminUrl | 管理画面の会員詳細URL | urlUtils.urlFor('admin') + '/members/{id}' | 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 |
| adminUrl | 管理画面URL | urlUtils.urlFor('admin', true) | Yes |
| staffUrl | スタッフ設定URL | urlUtils.urlFor('admin') + '/settings/staff/{slug}/email-notifications' | Yes |
| toEmail | 受信者メールアドレス | user.email | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| ドメインイベント | DonationPaymentEvent | 常に | 一回払いのドネーションが処理された際に発火 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 通知対象ユーザーが0人 | `donation_notifications:true`のユーザーがいない場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[DonationPaymentEvent発火] --> B[通知対象ユーザー取得]
    B --> C{対象ユーザー存在?}
    C -->|No| Z[処理終了]
    C -->|Yes| D[金額フォーマット]
    D --> E{memberId存在?}
    E -->|Yes| F[会員データ取得]
    E -->|No| G[memberData = null]
    F --> H[各ユーザーにメール送信（並列）]
    G --> H
    H --> I[Promise.allSettledで結果収集]
    I --> J{失敗あり?}
    J -->|Yes| K[失敗をログに記録]
    K --> Z
    J -->|No| Z
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| members | 会員情報取得（会員の場合） | id, name, email |
| users | 通知対象スタッフ取得 | status:active, donation_notifications:true |
| settings | サイト設定取得 | title, accent_color |

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

#### DonationPaymentEvent（イベントデータ）

| 参照項目 | 用途 | 取得条件 |
|---------|------|---------|
| memberId | 会員情報取得 | 存在する場合のみ |
| name | ドネーション送信者名 | - |
| email | ドネーション送信者メール | - |
| amount | 金額（セント単位） | - |
| currency | 通貨コード | - |
| donationMessage | ドネーションメッセージ | オプショナル |

### 更新テーブル一覧

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

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | SMTP接続エラー等 | logging.warnでログ出力、他のユーザーへの送信は継続 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | なし（Promise.allSettledで結果を収集するのみ） |
| リトライ間隔 | - |
| リトライ対象エラー | - |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし |
| 1日あたり上限 | 制限なし |

### 配信時間帯

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

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

- 決済情報（金額）とドネーション送信者のメールアドレスが含まれる
- 「Reply」/「Say thanks」ボタンはmailto:リンクで、ドネーション送信者への直接メール送信を可能にする
- 送信先はスタッフメンバーのみに限定

## 備考

- 金額は`amount / 100`で計算（Stripeはセント単位）
- ドネーションメッセージの有無でCTAボタンのテキストが変化（Reply / Say thanks）
- `memberId`が存在する場合のみ会員情報リンクを表示
- 件名の絵文字は`\ud83d\udcb0`（お金の袋）

---

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

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

### 推奨読解順序

#### Step 1: イベントデータ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | donation-payment-event.js | `ghost/core/core/server/services/donations/donation-payment-event.js` | DonationPaymentEventのデータ構造（memberId, name, email, amount, currency, donationMessage） |

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | staff-service-emails.js | `ghost/core/core/server/services/staff/staff-service-emails.js` | 270-323行目: `notifyDonationReceived`メソッド（JSDocで型参照あり） |

**主要処理フロー**:
1. **272行目**: 通知対象ユーザー取得
2. **273行目**: 金額をフォーマット（/100でセントからドルへ変換）
3. **275-281行目**: 件名の構築
4. **276-280行目**: memberIdがある場合のみmemberData生成

#### Step 3: テンプレート処理

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | donation.hbs | `ghost/core/core/server/services/staff/email-templates/donation.hbs` | 全体構造、メッセージ有無によるCTAボタンの切り替え（56-76行目） |

**主要処理フロー**:
- **31行目**: 見出し「Cha-ching! You received a tip.」
- **40行目**: memberDataがある場合のViewリンク
- **43-45行目**: donationMessageの表示
- **56-76行目**: メッセージ有無でReply/Say thanksを切り替え

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

```
DonationPaymentEvent（ドメインイベント発火）
    │
    └─ StaffServiceEmails.notifyDonationReceived()
           │
           ├─ models.User.getEmailAlertUsers('donation')
           │
           ├─ getFormattedAmount(amount / 100, currency)
           │
           ├─ getMemberData() - memberIdがある場合のみ
           │
           ├─ renderEmailTemplate('donation')
           │
           └─ Promise.allSettled(sendMail(...))
```

### データフロー図

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

DonationPaymentEvent ───▶ StaffServiceEmails ───▶ スタッフメンバーへのメール
  │                           │
  ├─ memberId                ├─ 金額フォーマット
  ├─ name                    ├─ 会員データ取得（条件付き）
  ├─ email                   ├─ テンプレートレンダリング
  ├─ amount                  └─ 並列メール送信
  ├─ currency
  └─ donationMessage

[出力メール]
  ├─ 件名: 絵文字 + 金額 + 送信者名
  ├─ 本文: 金額、送信者、メッセージ
  └─ CTA: mailto:リンク（Reply/Say thanks）
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| staff-service-emails.js | `ghost/core/core/server/services/staff/staff-service-emails.js` | ソース | メール送信ロジック |
| donation.hbs | `ghost/core/core/server/services/staff/email-templates/donation.hbs` | テンプレート | HTMLメールテンプレート |
| donation.txt.js | `ghost/core/core/server/services/staff/email-templates/donation.txt.js` | テンプレート | テキストメールテンプレート |
| donation-payment-event.js | `ghost/core/core/server/services/donations/donation-payment-event.js` | ソース | DonationPaymentEventの定義 |
