# 通知設計書 6-メンション通知

## 概要

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

### 本通知の処理概要

カードやコメント内でユーザーがメンション（@username形式）された際に、該当ユーザーに対してアプリ内通知を送信する機能を提供する。

**業務上の目的・背景**：チームコラボレーションにおいて、特定のメンバーに対して直接注意を向けたい場合がある。質問を特定の人に向ける、タスクの確認を依頼する、意見を求めるなど、メンションは重要なダイレクトコミュニケーション手段である。メンション通知がなければ、自分宛てのメッセージに気づかず、重要なコミュニケーションが成立しないリスクがある。

**通知の送信タイミング**：カードまたはコメントにメンションが含まれた際に、Mentionモデルが作成され、そのafter_create_commitコールバックによりNotifyRecipientsJobが非同期で実行される。

**通知の受信者**：メンションされたユーザー（mentionee）が受信者となる。ただし、自分自身をメンションした場合（self_mention）は通知対象外となる。

**通知内容の概要**：通知には、メンションしたユーザーの名前、メンションが含まれる本文の抜粋（200文字まで）、対象カードへのリンクが含まれる。

**期待されるアクション**：受信者は通知を確認し、メンションされた文脈を確認、必要に応じて返信、タスクの対応、質問への回答などのアクションを行うことが期待される。

## 通知種別

アプリ内通知

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（Active Job） |
| 優先度 | 高 |
| リトライ | Active Jobのデフォルト設定に準拠 |

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

1. Mentionレコードからmentioneeを取得
2. 自己メンション（mentioner == mentionee）の場合は除外

```ruby
if mention.self_mention?
  []
else
  [ mention.mentionee ]
end
```

## 通知テンプレート

### アプリ内通知

| 項目 | 内容 |
|-----|------|
| 表示形式 | リスト形式（通知一覧画面） |
| アバター | メンション実行者のアバター画像 |
| リンク先 | カード詳細画面 |

### 本文テンプレート

Webプッシュ通知連動時のメッセージ:
```
Title: "{メンション実行者のファーストネーム} mentioned you"
Body: {メンション含む本文の抜粋（最大200文字）}
```

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| mentioner.first_name | メンション実行者のファーストネーム | users.name | Yes |
| mention.source.mentionable_content | メンション含む本文 | action_text_rich_texts.body | Yes |
| card.title | カードのタイトル | cards.title | No |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | カード作成/編集時のメンション | Mention作成 | カード本文に@usernameが含まれた際 |
| 画面操作 | コメント投稿時のメンション | Mention作成 | コメント本文に@usernameが含まれた際 |
| API | コンテンツ作成/更新API | Mention作成 | APIを通じてメンションが追加された際 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| self_mention? = true | 自分自身をメンションした場合は通知しない |
| creator.system? = true | システムユーザーによる操作の場合は通知しない |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[メンション作成] --> B[Mention作成]
    B --> C[after_create_commit]
    C --> D[NotifyRecipientsJob キュー投入]
    D --> E[Notifier.for source 実行]
    E --> F[Notifier::MentionNotifier 生成]
    F --> G[should_notify? チェック]
    G -->|creator.system? = false| H[recipients 決定]
    G -->|creator.system? = true| I[処理終了]
    H --> J{self_mention?}
    J -->|Yes| K[空配列返却]
    J -->|No| L[mentionee返却]
    K --> I
    L --> M[Notification作成/更新]
    M --> N[after_create_commit: bundle処理]
    N --> O[after_save_commit: PushNotificationJob]
    O --> P[watch_source_by_mentionee]
    P --> Q[Turbo Stream配信]
    Q --> R[終了]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| mentions | メンション情報取得 | source_typeがMentionの場合 |
| users | ユーザー情報取得 | mentioner, mentionee |
| cards | カード情報取得 | mention.source.card |
| comments | コメント情報取得 | source_typeがCommentの場合 |
| action_text_rich_texts | 本文情報取得 | mentionable_content |

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

#### mentions

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | メンション識別子 | 通知のsource_id |
| mentioner_id | メンション実行者ID | 通知除外判定、表示用 |
| mentionee_id | メンションされたユーザーID | 通知受信者 |
| source_type | ソース種別 | Card/Comment |
| source_id | ソースID | カード/コメント取得 |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| notifications | INSERT/UPDATE | 通知レコードの作成または更新 |
| notification_bundles | INSERT/UPDATE | メールバンドル用レコード |
| watches | INSERT | メンションされたユーザーをウォッチャーに追加 |

#### notificationsテーブル

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| INSERT/UPDATE | user_id | mentioneeのID | 通知対象ユーザー |
| INSERT/UPDATE | card_id | 対象カードのID | set_cardで自動設定 |
| INSERT/UPDATE | source_type | "Mention" | ポリモーフィック関連 |
| INSERT/UPDATE | source_id | MentionのID | メンション参照 |
| INSERT/UPDATE | creator_id | mentionerのID | 通知作成者 |
| INSERT/UPDATE | unread_count | 1（新規）/ +1（更新） | 未読カウント |
| UPDATE | read_at | nil | 再度未読にする |

#### watchesテーブル

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| INSERT | user_id | mentioneeのID | ウォッチするユーザー |
| INSERT | card_id | 対象カードのID | ウォッチ対象カード |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| ActiveJob::DeserializationError | 通知対象レコードが削除済み | PushNotificationJobでdiscard |
| ActiveRecord::RecordNotUnique | 同一user_id, card_idの重複 | create_or_find_byで対応済み |
| ActiveRecord::RecordInvalid | バリデーションエラー | 例外発生、ジョブリトライ |

### リトライ仕様

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

## 配信設定

### レート制限

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

### 配信時間帯

制限なし（24時間配信可能）

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

- 通知はaccount_idでスコープされ、同一アカウント内のユーザーにのみ配信される
- メンションは同一アカウント内のユーザーのみ対象となる
- メンション本文は抜粋として通知に含まれるが、200文字に制限される

## 備考

- メンションされたユーザーは自動的にソース（カード/コメント）のウォッチャーになる
- イベント通知（card_published, comment_created等）では、メンションされたユーザーは受信者から除外される（メンション通知と重複を防ぐため）
- 同一カードに対する複数回のメンションでは、通知が更新されunread_countがインクリメントされる

---

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

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

### 推奨読解順序

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

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | notification.rb | `app/models/notification.rb` | 通知モデルの構造、source_typeが"Mention"の場合 |
| 1-2 | mention.rb | `app/models/mention.rb` | メンションモデル、Notifiableインクルード（1-25行目） |
| 1-3 | schema.rb | `db/schema.rb` | mentions テーブル定義（359-371行目） |

**読解のコツ**: MentionモデルはNotifiableをインクルードしており、after_create_commitでnotify_recipients_laterが呼ばれる。

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

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | notifiable.rb | `app/models/concerns/notifiable.rb` | after_create_commitでnotify_recipients_laterが呼ばれる |
| 2-2 | notifier.rb | `app/models/notifier.rb` | sourceがMentionの場合、MentionNotifierを生成（9-10行目） |

**主要処理フロー**:
1. **9-10行目（notifier.rb）**: `when Mention then MentionNotifier.new(source)` でMentionNotifierを生成

#### Step 3: 通知生成ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | mention_notifier.rb | `app/models/notifier/mention_notifier.rb` | メンション通知の受信者決定ロジック（1-16行目） |
| 3-2 | notifier.rb | `app/models/notifier.rb` | notifyメソッドの実装 |

**主要処理フロー**:
- **5-11行目（mention_notifier.rb）**: 受信者決定ロジック
  ```ruby
  def recipients
    if mention.self_mention?
      []
    else
      [ mention.mentionee ]
    end
  end
  ```
- **13-15行目**: creatorの取得
  ```ruby
  def creator
    mention.mentioner
  end
  ```

#### Step 4: プッシュ通知ペイロードを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | notification_pusher.rb | `app/models/notification_pusher.rb` | メンション通知のペイロード構築（77-86行目） |

**主要処理フロー**:
- **77-86行目**: build_mention_payload
  ```ruby
  def build_mention_payload
    mention = notification.source
    card = mention.card
    {
      title: "#{mention.mentioner.first_name} mentioned you",
      body: format_excerpt(mention.source.mentionable_content, length: 200),
      path: card_path(card)
    }
  end
  ```

#### Step 5: ウォッチャー自動追加を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | mention.rb | `app/models/mention.rb` | メンションされたユーザーをウォッチャーに追加（9行目, 21-23行目） |

**主要処理フロー**:
- **9行目**: `after_create_commit :watch_source_by_mentionee`
- **21-23行目**: `source.watch_by(mentionee)` でウォッチャー追加

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

```
Mention#create (メンション作成時)
    │
    ├─ Notifiable#notify_recipients_later (after_create_commit)
    │      └─ NotifyRecipientsJob.perform_later(self)
    │             └─ Notifier.for(mention)
    │                    └─ Notifier::MentionNotifier.new(mention)
    │                           └─ #notify
    │                                  ├─ #recipients
    │                                  │     ├─ self_mention? → []
    │                                  │     └─ else → [mentionee]
    │                                  └─ Notification.create_or_find_by
    │                                         ├─ #bundle (after_create)
    │                                         └─ PushNotificationJob.perform_later (after_save_commit)
    │                                               └─ NotificationPusher#push
    │                                                      └─ #build_mention_payload
    │                                                             └─ "{name} mentioned you"
    │
    └─ #watch_source_by_mentionee (after_create_commit)
           └─ source.watch_by(mentionee)
```

### データフロー図

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

Mention              ───▶ Notifier.for(mention)        ───▶ Notifier::MentionNotifier
  │                                                           │
  ├─ mentioner_id                                             ├─ self_mention?チェック
  ├─ mentionee_id                                             │   ├─ true → 通知なし
  ├─ source (Card/Comment)                                    │   └─ false → mentioneeに通知
  │     └─ mentionable_content                                │
  └─ card                                                     └─ Notification
                                                                    │
                                                                    ├─ notifications テーブル INSERT/UPDATE
                                                                    ├─ notification_bundles テーブル INSERT
                                                                    ├─ watches テーブル INSERT
                                                                    ├─ Turbo Stream broadcast
                                                                    └─ PushNotificationJob
                                                                          ├─ title: "{name} mentioned you"
                                                                          └─ body: {content抜粋}
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| notification.rb | `app/models/notification.rb` | モデル | 通知モデル定義 |
| mention.rb | `app/models/mention.rb` | モデル | メンションモデル定義 |
| notifier.rb | `app/models/notifier.rb` | モデル | 通知生成の基底クラス |
| mention_notifier.rb | `app/models/notifier/mention_notifier.rb` | モデル | メンション用通知生成 |
| notifiable.rb | `app/models/concerns/notifiable.rb` | Concern | 通知可能モデル用Concern |
| notification_pusher.rb | `app/models/notification_pusher.rb` | モデル | プッシュ通知送信処理（77-86行目） |
| schema.rb | `db/schema.rb` | スキーマ | テーブル定義（mentions: 359-371行目） |
