# 通知設計書 3-assigned_conversation_new_message

## 概要

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

### 本通知の処理概要

本通知は、エージェントが担当している会話に新しいメッセージが到着した際に、そのエージェントに即座に通知することで、顧客からの問い合わせへの迅速な対応を促すための機能である。

**業務上の目的・背景**：カスタマーサポート業務において、担当エージェントが顧客からの返信を見逃すと、対応の遅延や顧客満足度の低下につながる。本通知により、担当エージェントは自身が担当する会話の進展をリアルタイムで把握し、タイムリーな対応を行うことができる。特に複数の会話を同時に担当している場合に、新着メッセージの見落としを防止する効果がある。

**通知の送信タイミング**：メッセージ（Message）レコードが作成され、`MESSAGE_CREATED`イベントがディスパッチされた後、`NotificationListener#message_created`から`Messages::NewMessageNotificationService`が呼び出された時点で送信される。メッセージが`notifiable?`（incoming または outgoing）である場合のみ処理が進む。

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

**通知内容の概要**：新着メッセージがある会話のID、および会話へのリンクが含まれる。プッシュ通知ではメッセージ内容も表示される。

**期待されるアクション**：担当エージェントは通知を受け取った後、会話詳細ページにアクセスし、顧客への返信を行うことが期待される。

## 通知種別

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

## 送信仕様

### 基本情報

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

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

1. メッセージの`notifiable?`をチェック（incoming または outgoing であること）
2. 会話の担当者（assignee）を取得
3. 担当者が存在し、メッセージ送信者と異なることを確認
4. 既に同じメッセージに対して通知されていないことを確認（`already_notified?`）
5. 担当者に対して`NotificationBuilder`で通知を作成
6. コンタクトがブロックされている場合、通知は作成されない

## 通知テンプレート

### メール通知の場合

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

### 本文テンプレート

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

<p>You have received a new message in your assigned conversation.</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.assignee.blank?の場合 |
| 送信者が担当者本人 | conversation.assignee == sender の場合 |
| 既に通知済み | 同じメッセージに対する通知が既に存在する場合 |
| コンタクトがブロック済み | 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 -->|No| E[担当者通知スキップ]
    D -->|Yes| F{既に通知済み?}
    F -->|Yes| E
    F -->|No| G{送信者が担当者本人?}
    G -->|Yes| E
    G -->|No| H{コンタクトブロック確認}
    H -->|ブロック済み| E
    H -->|ブロックなし| I[Notification作成]
    I --> J[after_create_commit]
    J --> K{プッシュ購読?}
    K -->|Yes| L[PushNotificationJob]
    K -->|No| M{メール購読?}
    J --> M
    M -->|Yes| N{エージェントオンライン?}
    N -->|Yes| O[メール送信スキップ]
    N -->|No| P[EmailNotificationJob]
    M -->|No| Q[完了]
    L --> Q
    P --> Q
    O --> Q
    C --> Q
    E --> Q
```

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

### 参照テーブル一覧

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

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

#### messages

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

#### conversations

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | 会話特定 | メッセージから |
| assignee_id | 担当者特定 | 通知先決定 |
| display_id | 表示用ID | 通知本文用 |

### 更新テーブル一覧

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

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

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| INSERT | notification_type | 3 (assigned_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）のチェックにより、未確認アカウントへの送信を防止
- エージェントがオンラインの場合はメール通知をスキップしてスパム軽減

## 備考

- メンション通知が先に送信された場合、本通知は重複を避けるためスキップされる
- エージェントがオンライン状態の場合、メール通知はスキップされる（プッシュのみ）
- `participating_conversation_new_message`通知と同じサービス内で処理される
- secondary_actorにメッセージが設定されるため、プッシュ通知本文にメッセージ内容が表示される

---

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

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

### 推奨読解順序

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

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | notification.rb | `app/models/notification.rb` | NOTIFICATION_TYPESの定義（41行目: assigned_conversation_new_message: 3） |
| 1-2 | message_filter_helpers.rb | `app/models/concerns/message_filter_helpers.rb` | notifiable?メソッド（16-18行目） |

**読解のコツ**: `notifiable?`は`incoming?`または`outgoing?`のメッセージのみを対象とする。activityメッセージやテンプレートメッセージは対象外となる。

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

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

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

**主要処理フロー**:
1. **51行目**: extract_message_and_accountでメッセージを取得
2. **53行目**: Messages::MentionServiceを呼び出し（メンション通知が先）
3. **54行目**: Messages::NewMessageNotificationServiceを呼び出し

#### Step 3: 新規メッセージ通知サービスを理解する

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | new_message_notification_service.rb | `app/services/messages/new_message_notification_service.rb` | perform（4-9行目）、notify_conversation_assignee（15-27行目）、already_notified?（48-50行目） |

**主要処理フロー**:
- **5行目**: message.notifiable?でメッセージタイプをチェック
- **7行目**: notify_conversation_assigneeを呼び出し
- **16行目**: conversation.assignee.blank?で担当者存在チェック
- **17行目**: already_notified?で重複チェック
- **18行目**: 送信者が担当者本人かチェック
- **20-26行目**: NotificationBuilderで通知作成

#### Step 4: 重複チェックロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | new_message_notification_service.rb | `app/services/messages/new_message_notification_service.rb` | already_notified?（48-50行目） |

**主要処理フロー**:
- **49行目**: conversation.notifications.exists?(user: user, secondary_actor: message)で既存通知をチェック

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

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

**主要処理フロー**:
- **37行目**: OnlineStatusTracker.get_presenceでオンライン状態をチェック
- **38行目**: オンラインの場合はreturnでメール送信をスキップ

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

```
Message#after_create_commit (dispatch_create_events)
    |
    +-- dispatcher_dispatch(MESSAGE_CREATED)
           |
           +-- NotificationListener#message_created
                  |
                  +-- Messages::MentionService#perform (メンション通知が先)
                  |
                  +-- Messages::NewMessageNotificationService#perform
                         |
                         +-- message.notifiable? (チェック)
                         |
                         +-- notify_conversation_assignee
                                |
                                +-- conversation.assignee (担当者取得)
                                |
                                +-- already_notified? (重複チェック)
                                |
                                +-- NotificationBuilder#perform
                                       |
                                       +-- build_notification
                                              |
                                              +-- user.notifications.create!
                                                     |
                                                     +-- Notification#after_create_commit
                                                            |
                                                            +-- process_notification_delivery
                                                                   |
                                                                   +-- PushNotificationJob
                                                                   |
                                                                   +-- EmailNotificationJob
                                                                          |
                                                                          +-- OnlineStatusTracker.get_presence (チェック)
```

### データフロー図

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

Message                    NotificationListener                 Notification
(新規作成)        ───────▶  (message_created)        ───────▶    (DB保存)
       │                           │
       │                           ▼
       │                   NewMessageNotificationService
       │                   (通知判定・作成)
       │                           │
       │                           ▼
       │                   notifiable? / already_notified?
       │                   (判定ロジック)
       │                           │
       ▼                           ▼
 secondary_actor           NotificationBuilder
 (メッセージ参照)           (通知作成)
                                   │
                                   ▼
                           Notification Model
                           (after_create_commit)
                                   │
          ┌────────────────────────┼────────────────────────┐
          ▼                        ▼                        ▼
   PushNotificationJob     EmailNotificationJob     ActionCable
          │                        │                  (WebSocket)
          │                        ▼
          │                 OnlineStatusTracker
          │                 (オンラインチェック)
          ▼                        │
   プッシュ通知送信     メール送信（オフライン時のみ）
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| 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` | ソース | 新規メッセージ通知サービス |
| notification_builder.rb | `app/builders/notification_builder.rb` | ソース | 通知ビルダー |
| message_filter_helpers.rb | `app/models/concerns/message_filter_helpers.rb` | ソース | メッセージフィルターヘルパー |
| conversation_notifications_mailer.rb | `app/mailers/agent_notifications/conversation_notifications_mailer.rb` | ソース | メーラー |
| assigned_conversation_new_message.liquid | `app/views/mailers/agent_notifications/conversation_notifications_mailer/assigned_conversation_new_message.liquid` | テンプレート | メールテンプレート |
| message.rb | `app/models/message.rb` | ソース | メッセージモデル（イベント発火元） |
