# 通知設計書 2-conversation_assignment

## 概要

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

### 本通知の処理概要

本通知は、会話が特定のエージェントにアサインされた際に、そのエージェントに即座に通知することで、担当業務の認識と迅速な対応開始を促すための機能である。

**業務上の目的・背景**：カスタマーサポート業務において、会話のアサインメントは責任の明確化と効率的なワークフロー管理の基盤となる。本通知により、アサインされたエージェントは自身が担当すべき会話を即座に把握し、対応を開始できる。これにより、顧客を待たせることなく、スムーズな対応の引き継ぎや新規担当開始が可能となる。

**通知の送信タイミング**：会話の`assignee_id`が変更され、`CONVERSATION_UPDATED`イベントがディスパッチされた後、`NotificationListener#assignee_changed`メソッドが呼び出された時点で送信される。担当者変更が通知可能な変更（`notifiable_assignee_change`）と判定された場合のみ処理が進む。

**通知の受信者**：新しくアサインされた担当者（assignee）のみが受信者となる。自分自身にアサインした場合（セルフアサイン）は通知されない。

**通知内容の概要**：アサインされた会話のID、最近のメッセージ内容、および会話へのリンクが含まれる。

**期待されるアクション**：アサインされたエージェントは通知を受け取った後、会話詳細ページにアクセスし、顧客への返信や対応を開始することが期待される。

## 通知種別

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

## 送信仕様

### 基本情報

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

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

1. 会話の`assignee_id`変更を検知
2. `notifiable_assignee_change?`で通知可能な変更かを判定
   - `saved_change_to_assignee_id?`がtrueであること
   - `assignee_id`がblankでないこと
   - セルフアサインでないこと（`self_assign?`がfalse）
3. 会話ステータスがpendingでないことを確認
4. 新しい担当者（assignee）に対して`NotificationBuilder`で通知を作成
5. コンタクトがブロックされている場合、通知は作成されない

## 通知テンプレート

### メール通知の場合

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

### 本文テンプレート

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

<p>Time to save the world. A new conversation has been assigned to you</p>

{% for chat_message in conversation.recent_messages %}
  <div>
    <h4 style="margin: 0;">
      {% if chat_message.sender == user.available_name %}
        You
      {% else %}
        {{chat_message.sender}}
      {% endif %}
    </h4>
  </div>

  <div style="padding: 10px 20px; margin: 5px 0 20px 0; background: #F2F3F7; border-radius: 10px;">
    {% if chat_message.content %}
      {{chat_message.content}}
    {% endif %}

    {% if chat_message.attachments %}
      {% for attachment in chat_message.attachments %}
        Attachment [<a href="{{ attachment }}" _target="blank">Click here to view</a>]
      {% endfor %}
    {% endif %}
  </div>
{% endfor %}

<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 |
| conversation.recent_messages | 最新5件のメッセージ | messages (chat, last 5) | Yes |
| action_url | 会話詳細ページURL | 動的生成 | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| イベント | CONVERSATION_UPDATED | assignee_idが変更され、notifiable_assignee_change=true | 担当者変更時 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 担当者が空（nil） | assignee_idがblankの場合 |
| 会話ステータスがpending | ボット対応待ちの会話は通知しない |
| セルフアサイン | 自分自身にアサインした場合 |
| notifiable_assignee_changeがfalse | 通知不要な変更の場合 |
| コンタクトがブロック済み | contacts.blockedがtrueの場合 |
| メール通知時、既読の場合 | notification.read_atが設定済みの場合はメール送信しない |
| ユーザーがメール未確認 | user.confirmed_atがnilの場合はメール送信しない |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[CONVERSATION_UPDATEDイベント発生] --> B{assignee_id変更?}
    B -->|No| C[通知スキップ]
    B -->|Yes| D{担当者が存在?}
    D -->|No| C
    D -->|Yes| E{notifiable_assignee_change?}
    E -->|No| C
    E -->|Yes| F{会話ステータス確認}
    F -->|pending| C
    F -->|pending以外| G{コンタクトブロック確認}
    G -->|ブロック済み| C
    G -->|ブロックなし| H[Notification作成]
    H --> I[after_create_commit]
    I --> J{プッシュ購読?}
    J -->|Yes| K[PushNotificationJob]
    J -->|No| L{メール購読?}
    I --> L
    L -->|Yes| M[EmailNotificationJob]
    L -->|No| N[完了]
    K --> N
    M --> N
    C --> N
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| conversations | 会話情報取得 | primary_actor、assignee_id |
| users | エージェント情報 | 担当者情報 |
| notification_settings | 通知設定 | 購読状況確認 |
| contacts | コンタクト情報 | ブロック状態確認 |
| messages | メッセージ一覧 | テンプレート用 |

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

#### conversations

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | 会話特定 | イベントデータから |
| display_id | 表示用ID | 通知本文用 |
| account_id | アカウント特定 | 通知作成用 |
| assignee_id | 担当者特定 | 通知先決定 |
| status | ステータス確認 | pending判定 |
| contact_id | コンタクト特定 | ブロック確認用 |

### 更新テーブル一覧

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

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

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| INSERT | notification_type | 2 (conversation_assignment) | 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 | 'User' or nil | 操作者 |
| INSERT | secondary_actor_id | Current.user.id or nil | 操作者ID |
| 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）のチェックにより、未確認アカウントへの送信を防止

## 備考

- チーム変更により担当者がnilになる場合は通知されない（コード内コメント参照）
- セルフアサインの場合は通知されない
- `notifiable_assignee_change`フラグはイベントデータとして渡される

---

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

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

### 推奨読解順序

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

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | notification.rb | `app/models/notification.rb` | NOTIFICATION_TYPESの定義（40行目: conversation_assignment: 2） |
| 1-2 | conversation.rb | `app/models/conversation.rb` | notifiable_assignee_change?メソッド（182-188行目） |

**読解のコツ**: `notifiable_assignee_change?`メソッドは、担当者変更が通知すべき変更かどうかを判定する重要なロジックである。セルフアサインの除外や空値チェックがここで行われる。

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

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

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

**主要処理フロー**:
1. **31行目**: extract_conversation_and_accountで会話とアカウントを取得
2. **32行目**: conversation.assigneeで新しい担当者を取得
3. **38行目**: assignee.blank?で担当者の存在をチェック
4. **39行目**: event.data[:notifiable_assignee_change].blank?で通知可能な変更かチェック
5. **40行目**: conversation.pending?でステータスをチェック
6. **42-47行目**: NotificationBuilderで通知を作成

#### Step 3: 通知構築処理を理解する

NotificationBuilderの動作を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | notification_builder.rb | `app/builders/notification_builder.rb` | build_notification（25-38行目） |

**主要処理フロー**:
- **29行目**: ブロックされたコンタクトの会話は通知しない（conversation_mention除く）
- **31-37行目**: 実際のNotificationレコード作成

#### Step 4: 通知可能判定ロジックを理解する

会話モデルの判定ロジックを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | conversation.rb | `app/models/conversation.rb` | notifiable_assignee_change?（182-188行目）、dispatcher_dispatch（311-315行目） |

**主要処理フロー**:
- **183行目**: saved_change_to_assignee_id?でassignee_idの変更を検出
- **184行目**: assignee_id.blank?で空でないことを確認
- **185行目**: self_assign?でセルフアサインをチェック
- **312行目**: notifiable_assignee_changeをイベントデータに含める

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

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

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

```
Conversation#after_update_commit (notify_conversation_updation)
    |
    +-- dispatcher_dispatch(CONVERSATION_UPDATED, notifiable_assignee_change: true)
           |
           +-- NotificationListener#assignee_changed
                  |
                  +-- conversation.assignee (担当者取得)
                  |
                  +-- event.data[:notifiable_assignee_change] (チェック)
                  |
                  +-- NotificationBuilder#perform
                         |
                         +-- build_notification
                                |
                                +-- user.notifications.create!
                                       |
                                       +-- Notification#after_create_commit
                                              |
                                              +-- process_notification_delivery
                                                     |
                                                     +-- Notification::PushNotificationJob.perform_later
                                                     |
                                                     +-- Notification::EmailNotificationJob.perform_later
```

### データフロー図

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

Conversation               NotificationListener                 Notification
(assignee_id変更) ───────▶  (assignee_changed)       ───────▶    (DB保存)
       │                           │
       │                           ▼
       │                   notifiable_assignee_change?
       │                   (判定ロジック)
       │                           │
       ▼                           ▼
 notifiable_assignee_change  NotificationBuilder
 (イベントフラグ)             (通知作成)
                                   │
                                   ▼
                           Notification Model
                           (after_create_commit)
                                   │
          ┌────────────────────────┼────────────────────────┐
          ▼                        ▼                        ▼
   PushNotificationJob     EmailNotificationJob     ActionCable
          │                        │                  (dispatch_create_event)
          ▼                        ▼                        │
   プッシュ通知送信          メール送信               WebSocket通知
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| notification.rb | `app/models/notification.rb` | ソース | 通知モデル・配信トリガー |
| notification_listener.rb | `app/listeners/notification_listener.rb` | ソース | イベントリスナー |
| notification_builder.rb | `app/builders/notification_builder.rb` | ソース | 通知ビルダー |
| conversation.rb | `app/models/conversation.rb` | ソース | 会話モデル（判定ロジック含む） |
| push_notification_service.rb | `app/services/notification/push_notification_service.rb` | ソース | プッシュ通知サービス |
| email_notification_service.rb | `app/services/notification/email_notification_service.rb` | ソース | メール通知サービス |
| conversation_notifications_mailer.rb | `app/mailers/agent_notifications/conversation_notifications_mailer.rb` | ソース | メーラー |
| conversation_assignment.liquid | `app/views/mailers/agent_notifications/conversation_notifications_mailer/conversation_assignment.liquid` | テンプレート | メールテンプレート |
