# 通知設計書 4-participating_conversation_new_message

## 概要

本ドキュメントは、Chatwootシステムにおける「participating_conversation_new_message（参加会話新規メッセージ通知）」の設計仕様を記載したものである。参加している会話に新しいメッセージが作成された際に、会話参加者に通知を送信する機能の詳細を定義する。

### 本通知の処理概要

本通知は、会話に参加しているエージェント（担当者以外）が、その会話の新着メッセージを把握できるようにするための機能である。メンションや過去の関与により会話参加者（conversation_participants）として登録されたユーザーが対象となる。

**業務上の目的・背景**：カスタマーサポート業務において、一つの会話に複数のエージェントが関与することがある。例えば、メンションで呼ばれて助言を提供したエージェントや、以前担当していたエージェントなどである。本通知により、会話参加者は継続的に会話の進展を把握し、必要に応じて追加のサポートや情報提供を行うことができる。チームでの協力的な顧客対応を実現するための重要な機能である。

**通知の送信タイミング**：メッセージ（Message）レコードが作成され、`MESSAGE_CREATED`イベントがディスパッチされた後、`Messages::NewMessageNotificationService`の`notify_participating_users`メソッドが呼び出された時点で送信される。担当者への通知（assigned_conversation_new_message）の後に処理される。

**通知の受信者**：会話の参加者（conversation_participants）が受信者となる。ただし、メッセージの送信者本人には通知されない。また、既に同じメッセージに対して通知済み（担当者通知やメンション通知など）のユーザーには重複通知されない。

**通知内容の概要**：新着メッセージがある会話のID、および会話へのリンクが含まれる。

**期待されるアクション**：参加者は通知を受け取った後、必要に応じて会話詳細ページにアクセスし、追加の情報提供やサポートを行うことが期待される。

## 通知種別

- メール通知
- プッシュ通知（ブラウザプッシュ、FCMモバイルプッシュ）
- アプリ内通知（リアルタイム）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（ActiveJob） |
| 優先度 | 中（defaultキュー） |
| リトライ | Sidekiqデフォルト設定に準拠 |

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

1. メッセージの`notifiable?`をチェック（incoming または outgoing であること）
2. 会話の参加者（conversation_participants）を取得
3. 送信者がUserの場合、参加者リストから送信者を除外
4. 各参加者に対して、既に通知されていないことを確認（`already_notified?`）
5. 各参加者に対して`NotificationBuilder`で通知を作成
6. コンタクトがブロックされている場合、通知は作成されない

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | 環境変数`MAILER_SENDER_EMAIL`（デフォルト: accounts@chatwoot.com） |
| 送信元名称 | Chatwoot（環境変数で設定可能） |
| 件名 | `{agent_name}, New message in your participating conversation [ID - {display_id}].` |
| 形式 | HTML（Liquidテンプレート） |

### 本文テンプレート

```html
<p>Hi {{user.available_name}},</p>

<p>You have received a new message in a conversation you are participating.</p>

<p>Click <a href="{{action_url}}">here</a> to get cracking.</p>
```

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| なし | - | - | メール本文に会話へのリンクのみ含まれる |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| user.available_name | エージェントの表示名 | users.available_name | Yes |
| conversation.display_id | 会話の表示ID | conversations.display_id | Yes |
| action_url | 会話詳細ページURL | 動的生成 | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| イベント | MESSAGE_CREATED | メッセージがnotifiable? かつ 参加者が存在 | 新規メッセージ作成時 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| メッセージがnotifiable?でない | activity メッセージ等は対象外 |
| 参加者が存在しない | conversation_participantsが空の場合 |
| 送信者が参加者本人 | sender == participant の場合 |
| 既に通知済み | 同じメッセージに対する通知が既に存在する場合 |
| コンタクトがブロック済み | contacts.blockedがtrueの場合 |
| メール通知時、既読の場合 | notification.read_atが設定済みの場合はメール送信しない |
| メール通知時、エージェントがオンライン | OnlineStatusTrackerでオンライン判定の場合 |
| ユーザーがメール未確認 | user.confirmed_atがnilの場合はメール送信しない |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[MESSAGE_CREATEDイベント発生] --> B{メッセージがnotifiable?}
    B -->|No| C[通知スキップ]
    B -->|Yes| D[参加者リスト取得]
    D --> E{送信者がUser?}
    E -->|Yes| F[送信者を参加者リストから除外]
    E -->|No| G[参加者リストをそのまま使用]
    F --> H[各参加者に対してループ]
    G --> H
    H --> I{既に通知済み?}
    I -->|Yes| J[スキップ]
    I -->|No| K{コンタクトブロック確認}
    K -->|ブロック済み| J
    K -->|ブロックなし| L[Notification作成]
    L --> M[after_create_commit]
    M --> N{プッシュ購読?}
    N -->|Yes| O[PushNotificationJob]
    N -->|No| P{メール購読?}
    M --> P
    P -->|Yes| Q{エージェントオンライン?}
    Q -->|Yes| R[メール送信スキップ]
    Q -->|No| S[EmailNotificationJob]
    P -->|No| T[完了]
    O --> T
    S --> T
    R --> T
    J --> H
    C --> T
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| messages | メッセージ情報取得 | secondary_actor |
| conversations | 会話情報取得 | primary_actor |
| conversation_participants | 参加者情報取得 | 通知対象者 |
| users | エージェント情報 | 参加者情報 |
| notification_settings | 通知設定 | 購読状況確認 |
| contacts | コンタクト情報 | ブロック状態確認 |
| notifications | 既存通知確認 | 重複チェック |

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

#### conversation_participants

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| user_id | 参加者ユーザーID | conversation_idで検索 |

#### messages

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | メッセージ特定 | イベントデータから |
| message_type | notifiable判定 | incoming/outgoing |
| conversation_id | 会話特定 | FK |
| sender_type, sender_id | 送信者特定 | ポリモーフィック |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| notifications | INSERT | 通知レコード作成 |

#### 送信ログテーブル（notifications）

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| INSERT | notification_type | 5 (participating_conversation_new_message) | ENUM値 |
| INSERT | account_id | 会話のaccount_id | FK |
| INSERT | user_id | 参加者ID | FK |
| INSERT | primary_actor_type | 'Conversation' | ポリモーフィック |
| INSERT | primary_actor_id | 会話ID | FK |
| INSERT | secondary_actor_type | 'Message' | メッセージ |
| INSERT | secondary_actor_id | メッセージID | FK |
| INSERT | last_activity_at | created_at | 作成時刻 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗（SMTP） | SMTPサーバー接続エラー | ログ出力、例外キャッチ |
| WebPush期限切れ | WebPush::ExpiredSubscription | サブスクリプション削除 |
| WebPush無効 | WebPush::InvalidSubscription | サブスクリプション削除 |
| WebPush認証エラー | WebPush::Unauthorized | サブスクリプション削除 |
| WebPushレート制限 | WebPush::TooManyRequests | ログ警告出力 |
| FCMエラー | レスポンスにerror含む | サブスクリプション削除 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | Sidekiqデフォルト（25回） |
| リトライ間隔 | 指数バックオフ |
| リトライ対象エラー | 一時的なネットワークエラー等 |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし（システムレベル） |
| 1日あたり上限 | 制限なし |

### 配信時間帯

制限なし。24時間送信可能。

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

- メール送信はSMTP over TLS/SSLを推奨
- プッシュ通知はVAPID認証を使用
- FCM通知はFirebase認証を使用
- 通知内容に個人情報（メッセージ内容）が含まれるため、HTTPS必須
- ユーザー確認済み（confirmed_at）のチェックにより、未確認アカウントへの送信を防止
- エージェントがオンラインの場合はメール通知をスキップしてスパム軽減

## 備考

- プッシュ通知タイトルは`assigned_conversation_new_message`と同じI18nキーを使用（94行目参照）
- 担当者への通知が先に処理され、その後に参加者への通知が処理される
- メンション通知や担当者通知が既に送信されている場合、本通知はスキップされる
- 会話参加者は、メンションされた際に`MentionService`によって自動的に追加される
- secondary_actorにメッセージが設定されるため、プッシュ通知本文にメッセージ内容が表示される

---

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

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

### 推奨読解順序

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

通知システムの中核となるデータ構造を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | notification.rb | `app/models/notification.rb` | NOTIFICATION_TYPESの定義（43行目: participating_conversation_new_message: 5） |
| 1-2 | notification.rb | `app/models/notification.rb` | push_message_title（94行目: 同じI18nキーを使用） |

**読解のコツ**: 94行目で`participating_conversation_new_message`は`assigned_conversation_new_message`と同じI18nキー（`notifications.notification_title.assigned_conversation_new_message`）を使用していることに注意。

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

処理の起点となるリスナーを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | notification_listener.rb | `app/listeners/notification_listener.rb` | message_created（50-55行目） |

**主要処理フロー**:
1. **54行目**: Messages::NewMessageNotificationServiceを呼び出し

#### Step 3: 参加者通知ロジックを理解する

実際の通知ロジックを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | new_message_notification_service.rb | `app/services/messages/new_message_notification_service.rb` | notify_participating_users（29-44行目） |

**主要処理フロー**:
- **30行目**: conversation.conversation_participants.map(&:user)で参加者を取得
- **31行目**: participating_users -= [sender] if sender.is_a?(User)で送信者を除外
- **33行目**: participating_users.uniq.eachでループ
- **34行目**: already_notified?(participant)で重複チェック
- **36-42行目**: NotificationBuilderで通知作成

#### Step 4: 会話参加者の追加ロジックを理解する

参加者がどのように追加されるかを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | mention_service.rb | `app/services/messages/mention_service.rb` | add_mentioned_users_as_participants（63-67行目） |

**主要処理フロー**:
- **64-65行目**: メンションされたユーザーを会話参加者として追加

#### Step 5: メール通知を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | conversation_notifications_mailer.rb | `app/mailers/agent_notifications/conversation_notifications_mailer.rb` | participating_conversation_new_message（46-56行目） |
| 5-2 | participating_conversation_new_message.liquid | `app/views/mailers/agent_notifications/conversation_notifications_mailer/participating_conversation_new_message.liquid` | メールテンプレート全体 |

**主要処理フロー**:
- **49行目**: OnlineStatusTracker.get_presenceでオンライン状態をチェック

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

```
Message#after_create_commit (dispatch_create_events)
    |
    +-- dispatcher_dispatch(MESSAGE_CREATED)
           |
           +-- NotificationListener#message_created
                  |
                  +-- Messages::MentionService#perform
                  |      |
                  |      +-- add_mentioned_users_as_participants
                  |             (会話参加者に追加)
                  |
                  +-- Messages::NewMessageNotificationService#perform
                         |
                         +-- message.notifiable? (チェック)
                         |
                         +-- notify_conversation_assignee (担当者通知が先)
                         |
                         +-- notify_participating_users
                                |
                                +-- conversation.conversation_participants
                                |      (参加者リスト取得)
                                |
                                +-- participating_users -= [sender]
                                |      (送信者を除外)
                                |
                                +-- participating_users.uniq.each
                                       |
                                       +-- already_notified? (重複チェック)
                                       |
                                       +-- NotificationBuilder#perform
                                              |
                                              +-- Notification作成
```

### データフロー図

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

Message                    NotificationListener                 Notification
(新規作成)        ───────▶  (message_created)        ───────▶    (DB保存)
       │                           │
       │                           ▼
       │                   NewMessageNotificationService
       │                           │
       │              ┌────────────┴────────────┐
       │              ▼                         ▼
       │    notify_conversation_assignee  notify_participating_users
       │    (担当者通知)                   (参加者通知)
       │              │                         │
       │              ▼                         ▼
       │    already_notified?            conversation_participants
       │    (重複チェック)                (参加者取得)
       │                                        │
       │                                        ▼
       │                                参加者 - 送信者
       │                                (送信者除外)
       │                                        │
       ▼                                        ▼
 secondary_actor                      already_notified?
 (メッセージ参照)                      (重複チェック)
                                               │
                                               ▼
                                      NotificationBuilder
                                      (通知作成)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| notification.rb | `app/models/notification.rb` | ソース | 通知モデル・配信トリガー |
| notification_listener.rb | `app/listeners/notification_listener.rb` | ソース | イベントリスナー |
| new_message_notification_service.rb | `app/services/messages/new_message_notification_service.rb` | ソース | 新規メッセージ通知サービス |
| mention_service.rb | `app/services/messages/mention_service.rb` | ソース | メンションサービス（参加者追加） |
| notification_builder.rb | `app/builders/notification_builder.rb` | ソース | 通知ビルダー |
| conversation_notifications_mailer.rb | `app/mailers/agent_notifications/conversation_notifications_mailer.rb` | ソース | メーラー |
| participating_conversation_new_message.liquid | `app/views/mailers/agent_notifications/conversation_notifications_mailer/participating_conversation_new_message.liquid` | テンプレート | メールテンプレート |
| conversation_participant.rb | `app/models/conversation_participant.rb` | ソース | 会話参加者モデル |
