# 機能設計書 189-バルク自動割り当て

## 概要

本ドキュメントは、Chatwootにおける「バルク自動割り当て」機能の設計仕様を定義する。本機能は、自動割り当て対応インボックス内の未割り当て会話を、一括でエージェントに自動割り当てするためのスケジュールジョブ機能である。

### 本機能の処理概要

本機能は、定期的に実行されるスケジュールジョブとして、自動割り当てが有効なインボックス内の未割り当て会話を検出し、利用可能なエージェントにラウンドロビン方式で割り当てを行う。Assignment V2機能フラグが有効なアカウントでのみ動作する。

**業務上の目的・背景**：会話が発生した時点でリアルタイムに割り当てができない場合（エージェントが全員オフラインなど）でも、エージェントがオンラインになった時点で速やかに割り当てを行いたい。定期的なバルク割り当てにより、未割り当て会話の滞留を防ぎ、顧客への応答時間を短縮する。

**機能の利用シーン**：営業開始時刻に前日からの未割り当て会話を一括でエージェントに配分する。新しいエージェントがオンラインになった際に、待機中の会話を割り当てる。

**主要な処理内容**：
1. Assignment V2対応アカウントの取得
2. 自動割り当て有効インボックスの取得
3. 未割り当て・オープン状態の会話の取得
4. 容量に余裕があるエージェントへのラウンドロビン割り当て

**関連システム・外部連携**：Sidekiqによるスケジュールジョブ、Redisによるラウンドロビンキュー管理

**権限による制御**：システムジョブのため、ユーザー権限は不要

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | なし | - | バックグラウンドジョブのため画面なし |

## 機能種別

バッチ処理 / スケジュールジョブ

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| なし | - | - | スケジュールジョブのため入力パラメータなし | - |

### 入力データソース

- accounts テーブル（feature_assignment_v2スコープ）
- inboxes テーブル（enable_auto_assignment = true）
- conversations テーブル（unassigned, open）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| ログ出力 | String | 処理結果のログ |
| 会話更新 | - | assignee_idの更新 |

### 出力先

- Rails.logger（ログ）
- conversationsテーブル（更新）

## 処理フロー

### 処理シーケンス

```
1. ジョブ開始
   └─ scheduled_jobsキューから実行

2. アカウントループ
   ├─ feature_assignment_v2スコープで対象アカウント取得
   └─ 各アカウントに対して:
          ├─ should_skip_auto_assignment? チェック
          │      └─ Cloudかつdefault_planならスキップ
          └─ インボックスループ

3. インボックスループ
   └─ enable_auto_assignment = true のインボックス
          └─ process_assignment 実行

4. 割り当て処理
   ├─ member_ids_with_assignment_capacity で利用可能エージェント取得
   ├─ 利用可能エージェントがいない場合はスキップ
   └─ assign_conversations 実行

5. 会話割り当て
   └─ 未割り当て・オープン会話をLIMIT数だけ取得
          └─ 各会話に対してAgentAssignmentService実行
```

### フローチャート

```mermaid
flowchart TD
    A[ジョブ開始] --> B[feature_assignment_v2アカウント取得]
    B --> C{アカウントループ}

    C --> D{should_skip?}
    D -->|Yes| E[ログ出力、スキップ]
    D -->|No| F[インボックスループ]

    F --> G{enable_auto_assignment?}
    G -->|No| H[次のインボックス]
    G -->|Yes| I[process_assignment]

    I --> J[member_ids_with_assignment_capacity取得]
    J --> K{エージェントあり?}
    K -->|No| L[ログ出力、スキップ]
    K -->|Yes| M[assign_conversations]

    M --> N[未割り当て会話取得]
    N --> O[各会話にAgentAssignmentService]
    O --> P[ログ出力]

    E --> C
    H --> C
    L --> C
    P --> C
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-189-01 | Assignment V2必須 | feature_assignment_v2フラグが有効なアカウントのみ対象 | 常時 |
| BR-189-02 | Cloudスキップ | ChatwootCloudかつdefault_planのアカウントはスキップ | Cloud環境 |
| BR-189-03 | 容量考慮 | member_ids_with_assignment_capacityで容量を考慮したエージェント取得 | 常時 |
| BR-189-04 | LIMIT制限 | 1回の処理でLimits::AUTO_ASSIGNMENT_BULK_LIMIT件まで | 常時 |
| BR-189-05 | オンラインエージェント優先 | オンライン状態のエージェントにのみ割り当て | 常時 |

### 計算ロジック

ラウンドロビン割り当て:
- Redisのリストを使用してエージェントの順序を管理
- 割り当て後、そのエージェントをリストの末尾に移動

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| アカウント取得 | accounts | SELECT | feature_assignment_v2スコープ |
| インボックス取得 | inboxes | SELECT | enable_auto_assignment = true |
| 会話取得 | conversations | SELECT | unassigned, open, limit |
| 会話更新 | conversations | UPDATE | assignee_id設定 |

### テーブル別操作詳細

#### conversations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | * | inbox_id = @inbox.id, assignee_id IS NULL, status = 'open' | LIMIT付き |
| UPDATE | assignee_id | 割り当てエージェントID | |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | ログ出力 | 利用可能エージェントなし | ログ出力してスキップ |
| - | ログ出力 | スキップ対象アカウント | ログ出力してスキップ |
| 500 | Internal Server Error | ジョブ実行エラー | Sidekiqリトライ |

### リトライ仕様

Sidekiqのデフォルトリトライ設定に従う。

## トランザクション仕様

各会話の更新は個別のトランザクションで実行。

## パフォーマンス要件

- scheduled_jobsキューで実行
- LIMIT制限により1回の処理量を制限
- find_eachでメモリ効率を確保

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

- システムジョブのためユーザー認証不要
- アカウント境界はfind_eachで自動的に保証

## 備考

- Limits::AUTO_ASSIGNMENT_BULK_LIMITで処理件数を制限
- BillingHelperを使用してプラン判定

---

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

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

### 推奨読解順序

#### Step 1: ジョブ実装を理解する

まず、スケジュールジョブの実装を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | bulk_auto_assignment_job.rb | `app/jobs/inboxes/bulk_auto_assignment_job.rb` | ジョブ実装 |

**読解のコツ**:
- **1-3行目**: ジョブクラス定義、キュー設定、BillingHelper
- **5-15行目**: performメソッド - メインループ
- **20-29行目**: process_assignment - インボックス処理
- **31-39行目**: assign_conversations - 会話割り当て処理
- **41-46行目**: should_skip_auto_assignment? - スキップ判定

#### Step 2: 割り当てサービスを理解する

実際の割り当てロジックを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | agent_assignment_service.rb | `app/services/auto_assignment/agent_assignment_service.rb` | 割り当てサービス |
| 2-2 | inbox_round_robin_service.rb | `app/services/auto_assignment/inbox_round_robin_service.rb` | ラウンドロビン実装 |

**主要処理フロー**:
- **agent_assignment_service.rb 7-9行目**: find_assignee - 利用可能エージェント検索
- **agent_assignment_service.rb 11-14行目**: perform - 割り当て実行
- **inbox_round_robin_service.rb 28-32行目**: available_agent - ラウンドロビンでエージェント選択

#### Step 3: Redisキュー管理を理解する

ラウンドロビンのキュー管理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | inbox_round_robin_service.rb | `app/services/auto_assignment/inbox_round_robin_service.rb` | Redisリスト操作 |

**主要処理フロー**:
- **6-7行目**: clear_queue - キュークリア
- **10-12行目**: add_agent_to_queue - エージェント追加
- **14-16行目**: remove_agent_from_queue - エージェント削除
- **44-49行目**: pop_push_to_queue - 割り当て後の順序更新

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

```
Sidekiq Scheduler
    │
    └─ Inboxes::BulkAutoAssignmentJob#perform
           │
           ├─ Account.feature_assignment_v2.find_each
           │      │
           │      └─ should_skip_auto_assignment?
           │             └─ BillingHelper#default_plan?
           │
           └─ account.inboxes.where(enable_auto_assignment: true).find_each
                  │
                  └─ process_assignment(inbox)
                         │
                         ├─ inbox.member_ids_with_assignment_capacity
                         │
                         └─ assign_conversations(inbox, allowed_agent_ids)
                                │
                                └─ AutoAssignment::AgentAssignmentService#perform
                                       │
                                       ├─ find_assignee
                                       │      └─ round_robin_manage_service.available_agent
                                       │             └─ InboxRoundRobinService
                                       │                    └─ Redis操作
                                       │
                                       └─ conversation.update(assignee: new_assignee)
```

### データフロー図

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

Sidekiq Scheduler ────▶ BulkAutoAssignmentJob ──────────▶ ログ出力
                              │
                              ▼
                     アカウント・インボックス取得
                              │
                              ▼
                     未割り当て会話取得
                     (conversations.unassigned.open)
                              │
                              ▼
                     AgentAssignmentService
                              │
                    ┌─────────┴─────────┐
                    ▼                   ▼
              Redis                conversations
          (round_robin_key)        (assignee_id更新)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| bulk_auto_assignment_job.rb | `app/jobs/inboxes/bulk_auto_assignment_job.rb` | ジョブ | スケジュールジョブ |
| agent_assignment_service.rb | `app/services/auto_assignment/agent_assignment_service.rb` | サービス | 割り当てロジック |
| inbox_round_robin_service.rb | `app/services/auto_assignment/inbox_round_robin_service.rb` | サービス | ラウンドロビン管理 |
| billing_helper.rb | `app/helpers/billing_helper.rb` | ヘルパー | プラン判定 |
