# 通知設計書 8-通知バンドルメール

## 概要

本ドキュメントは、Fizzyアプリケーションにおける「通知バンドルメール」機能について、その設計仕様を記載するものである。

### 本通知の処理概要

複数のアプリ内通知をまとめて定期的にメール送信する機能を提供する。ユーザーが設定した頻度（数時間ごと/毎日/毎週）に応じて、未読通知をバンドルしてメールで配信する。

**業務上の目的・背景**：アプリ内通知やプッシュ通知だけでは、ユーザーがアプリケーションにアクセスしない限り通知を見落とす可能性がある。バンドルメールにより、一定期間の通知をまとめてメールで送信することで、アプリケーション外でも重要な更新情報を確認でき、見落としを防止する。また、個別に通知メールを送信するのではなくバンドルすることで、メール疲れを軽減し、ユーザー体験を向上させる。

**通知の送信タイミング**：ユーザーが設定した頻度に基づいてNotification::Bundleが作成され、バンドルの終了時刻（ends_at）に達した時点でNotification::Bundle.deliver_allジョブが実行され、メールが送信される。

**通知の受信者**：バンドルメール設定が有効（bundle_email_frequencyがnever以外）で、アクティブかつ認証済みのユーザー、かつシステムユーザーでないユーザーが受信者となる。

**通知内容の概要**：メールには、バンドル開始時刻以降の未読通知一覧が含まれる。通知はボードごと、カードごとにグループ化されて表示される。

**期待されるアクション**：受信者はメールを確認し、重要な通知がないかチェック、必要に応じてFizzyアプリケーションにアクセスして詳細を確認・対応を行うことが期待される。

## 通知種別

メール通知

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（Active Job） |
| 優先度 | 中 |
| リトライ | Active Jobのデフォルト設定に準拠 |
| 送信間隔 | every_few_hours: 4時間, daily: 24時間, weekly: 1週間 |

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

1. Notification::Bundle.dueで配信対象のバンドルを取得
2. 各バンドルに対して以下を確認:
   - ユーザー設定でbundling_emails?がtrue
   - バンドル内に未読通知が存在
   - アカウントがアクティブ

```ruby
def deliverable?
  user.settings.bundling_emails? && notifications.any? && account.active?
end
```

## 通知テンプレート

### メール通知

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | support@fizzy.do（環境変数で設定可能） |
| 送信元名称 | Fizzy |
| 件名 | "Fizzy: New notifications" または "Fizzy ({アカウント名}): New notifications" |
| 形式 | HTML/テキスト両対応 |

### 本文テンプレート

HTML形式:
```html
<h1>Notifications since {バンドル開始日時}</h1>
<p>You have {通知数} new notification(s) in {アカウント名}.</p>

{ボードごとにグループ化}
<h2>{ボード名}</h2>
  {カードごとにグループ化}
  #{カード番号} {カードタイトル}
    {通知内容}

<footer>
  Fizzy emails you about new notifications every few hours.
  Change how often you get these or unsubscribe from all email notifications.
</footer>
```

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| @bundle.starts_at | バンドル開始日時 | notification_bundles.starts_at | Yes |
| @notifications | 通知一覧 | bundle.notifications | Yes |
| @user | 受信ユーザー | bundle.user | Yes |
| @unsubscribe_token | 購読解除トークン | user.generate_token_for(:unsubscribe) | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| スケジュール | Notification::Bundle.deliver_all | ends_at <= 現在時刻 | 定期ジョブで配信対象バンドルを処理 |
| 画面操作 | メール頻度設定変更 | flush処理 | 設定変更時に保留中バンドルを即時送信 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| bundle_email_frequency = never | メール通知が無効の場合 |
| user.system? = true | システムユーザーの場合 |
| user.active? = false | ユーザーが非アクティブの場合 |
| user.verified? = false | ユーザーが未認証の場合 |
| notifications.empty? | 対象期間に未読通知がない場合 |
| account.active? = false | アカウントが非アクティブの場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[Notification作成] --> B[after_create: bundle]
    B --> C{bundling_emails?}
    C -->|Yes| D[find_or_create_bundle_for]
    C -->|No| E[処理終了]
    D --> F[Notification::Bundle作成/更新]
    F --> G[待機（ends_atまで）]
    G --> H[Notification::Bundle.deliver_all]
    H --> I[due.in_batches]
    I --> J[DeliverJob実行]
    J --> K{deliverable?}
    K -->|Yes| L[BundleMailer.notification送信]
    K -->|No| M[delivered!のみ]
    L --> N[delivered!]
    M --> N
    N --> O[終了]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| notification_bundles | バンドル情報取得 | 配信対象特定 |
| notifications | 通知情報取得 | バンドル内通知 |
| users | ユーザー情報取得 | 受信者情報 |
| user_settings | 設定情報取得 | メール頻度設定 |
| accounts | アカウント情報取得 | アクティブ状態 |
| cards | カード情報取得 | 通知内容表示 |
| boards | ボード情報取得 | グループ化表示 |

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

#### notification_bundles

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | バンドルID | 処理対象特定 |
| user_id | ユーザーID | 受信者特定 |
| starts_at | バンドル開始日時 | 通知範囲の始点 |
| ends_at | バンドル終了日時 | 配信判定、通知範囲の終点 |
| status | ステータス | pending/processing/delivered |

#### user_settings

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| bundle_email_frequency | メール頻度設定 | never/every_few_hours/daily/weekly |
| timezone_name | タイムゾーン | メール送信時のタイムゾーン |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| notification_bundles | INSERT/UPDATE | バンドル作成・ステータス更新 |

#### notification_bundlesテーブル

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| INSERT | user_id | ユーザーID | バンドル所有者 |
| INSERT | starts_at | 通知作成日時 | バンドル開始 |
| INSERT | ends_at | starts_at + 集約期間 | バンドル終了 |
| INSERT | status | 0 (pending) | 初期状態 |
| UPDATE | status | 1 (processing) | 処理中 |
| UPDATE | status | 2 (delivered) | 配信完了 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| ActiveJob::DeserializationError | バンドルが削除済み | discardまたはリトライ |
| Net::SMTPError | SMTP接続エラー | リトライ |
| ActionMailer::DeliveryError | メール送信エラー | リトライ |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | Active Jobデフォルト（5回） |
| リトライ間隔 | 指数バックオフ |
| リトライ対象エラー | StandardError |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | SMTPサーバー設定に依存 |
| 1日あたり上限 | 制限なし |

### 配信時間帯

制限なし（バンドル終了時刻に配信）

### 頻度設定と集約期間

| 設定値 | 説明 | 集約期間 |
|-------|------|---------|
| never | メール通知無効 | - |
| every_few_hours | 数時間ごと | 4時間 |
| daily | 毎日 | 24時間 |
| weekly | 毎週 | 1週間 |

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

- 購読解除トークンは1ヶ月で期限切れ（`generates_token_for :unsubscribe, expires_in: 1.month`）
- メール送信時はユーザーのタイムゾーンを考慮
- メール内容はアカウントスコープでフィルタリング

## 備考

- バンドルは重複しないようにoverlapping_with検証あり
- メール頻度設定変更時は保留中バンドルがflush（即時送信）される
- テンプレートはHTML/テキスト両方用意されている
- Unsubscribableモジュールで購読解除リンクが自動挿入される

---

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

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

### 推奨読解順序

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

まず、バンドルメールで使用されるデータモデルの構造を理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | notification/bundle.rb | `app/models/notification/bundle.rb` | バンドルモデル、ステータス管理、配信ロジック（1-83行目） |
| 1-2 | user/settings.rb | `app/models/user/settings.rb` | メール頻度設定、集約期間（1-57行目） |
| 1-3 | schema.rb | `db/schema.rb` | notification_bundles, user_settingsテーブル定義（373-385行目, 765-775行目） |

**読解のコツ**: バンドルはstarts_atとends_atで時間窓を定義し、その範囲内の未読通知を集約する。

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

処理の起点となるファイル・関数を特定する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | notification.rb | `app/models/notification.rb` | after_create :bundle コールバック（22行目） |
| 2-2 | user/notifiable.rb | `app/models/user/notifiable.rb` | bundle メソッドの実装（11-15行目） |

**主要処理フロー**:
1. **22行目（notification.rb）**: `after_create :bundle`
2. **58-60行目（notification.rb）**: `user.bundle(self) if user.settings.bundling_emails?`

#### Step 3: バンドル作成ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | user/notifiable.rb | `app/models/user/notifiable.rb` | find_or_create_bundle_for ロジック（17-35行目） |

**主要処理フロー**:
- **18-19行目**: `find_bundle_for(notification) || expand_pending_bundle_for(notification) || create_bundle_for(notification)`

#### Step 4: 配信ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | notification/bundle.rb | `app/models/notification/bundle.rb` | deliver_all, deliver メソッド（22-49行目） |

**主要処理フロー**:
- **22-28行目**: deliver_all（バッチ処理）
  ```ruby
  def deliver_all
    due.in_batches do |batch|
      jobs = batch.collect { DeliverJob.new(it) }
      ActiveJob.perform_all_later jobs
    end
  end
  ```
- **39-49行目**: deliver（個別配信）
  ```ruby
  def deliver
    user.in_time_zone do
      Current.with_account(user.account) do
        processing!
        Notification::BundleMailer.notification(self).deliver if deliverable?
        delivered!
      end
    end
  end
  ```

#### Step 5: メーラーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | notification/bundle_mailer.rb | `app/mailers/notification/bundle_mailer.rb` | メール生成ロジック（1-20行目） |
| 5-2 | notification.html.erb | `app/views/mailers/notification/bundle_mailer/notification.html.erb` | HTMLテンプレート |

**主要処理フロー**:
- **6-18行目**: notification メソッド
  ```ruby
  def notification(bundle)
    @user = bundle.user
    @bundle = bundle
    @notifications = bundle.notifications
      .preload(:card, :creator, source: [ :board, :creator ])
      .reject { |n| n.source.nil? || n.card.nil? }
    @unsubscribe_token = @user.generate_token_for(:unsubscribe)

    if @notifications.any?
      mail \
        to: bundle.user.identity.email_address,
        subject: "Fizzy#{ " (#{ Current.account.name })" if @user.identity.accounts.many? }: New notifications"
    end
  end
  ```

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

```
Notification#create
    │
    ├─ #bundle (after_create)
    │      └─ user.bundle(self)
    │             └─ User::Notifiable#bundle
    │                    └─ find_or_create_bundle_for(notification)
    │                           ├─ find_bundle_for → 既存バンドルに含める
    │                           ├─ expand_pending_bundle_for → 時間窓を拡張
    │                           └─ create_bundle_for → 新規バンドル作成
    │                                  └─ Notification::Bundle.create!(...)
    │
    │  === 時間経過（ends_atまで）===
    │
    └─ Notification::Bundle.deliver_all（定期ジョブ）
           └─ due.in_batches
                  └─ DeliverJob.perform_later
                         └─ bundle.deliver
                                ├─ processing!
                                ├─ deliverable? チェック
                                │     ├─ bundling_emails?
                                │     ├─ notifications.any?
                                │     └─ account.active?
                                ├─ BundleMailer.notification(self).deliver
                                │     ├─ @notifications = bundle.notifications
                                │     ├─ @unsubscribe_token生成
                                │     └─ mail送信
                                └─ delivered!
```

### データフロー図

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

Notification         ───▶ User#bundle                   ───▶ Notification::Bundle
  │                         │                                  │
  ├─ user_id                ├─ bundling_emails?判定            ├─ starts_at
  ├─ updated_at             ├─ find_or_create_bundle_for       ├─ ends_at
  └─ card_id                │                                  └─ status: pending
                            │
                            │  === 時間経過 ===
                            │
Notification::Bundle ───▶ deliver_all                   ───▶ BundleMailer
  │                         │                                  │
  ├─ starts_at              ├─ due バンドル取得                 ├─ @notifications
  ├─ ends_at                ├─ deliverable? 判定               ├─ @bundle
  ├─ user_id                │                                  ├─ @unsubscribe_token
  └─ status                 │                                  │
                            │                                  └─ メール送信
User::Settings       ───────┤                                        │
  │                         │                                        ▼
  └─ bundle_email_frequency │                              受信者のメールボックス
       └─ 集約期間決定      └─ processing! → delivered!
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| notification/bundle.rb | `app/models/notification/bundle.rb` | モデル | バンドルモデル定義 |
| notification/bundle_mailer.rb | `app/mailers/notification/bundle_mailer.rb` | メーラー | メール送信 |
| user/settings.rb | `app/models/user/settings.rb` | モデル | ユーザー設定 |
| user/notifiable.rb | `app/models/user/notifiable.rb` | Concern | ユーザーの通知関連処理 |
| notification.rb | `app/models/notification.rb` | モデル | 通知モデル（bundleコールバック） |
| notification.html.erb | `app/views/mailers/notification/bundle_mailer/notification.html.erb` | ビュー | HTMLテンプレート |
| notification.text.erb | `app/views/mailers/notification/bundle_mailer/notification.text.erb` | ビュー | テキストテンプレート |
| schema.rb | `db/schema.rb` | スキーマ | テーブル定義 |
