# バッチ設計書 12-AutoAssignment::AssignmentJob

## 概要

本ドキュメントは、Chatwootシステムにおける個別インボックスへの自動アサイン処理を実行するバッチ処理の設計を記述するものです。

### 本バッチの処理概要

本バッチは、指定されたインボックスに対して、未アサインの会話を利用可能なエージェントに自動的に割り当てる処理を実行します。

**業務上の目的・背景**：カスタマーサポート業務において、新規に受信した会話を適切なエージェントに迅速にアサインすることは、顧客満足度向上の重要な要素です。本バッチは、設定されたアサインポリシー（ラウンドロビン、レート制限など）に基づいて、自動的にエージェントへ会話を割り当てることで、手動アサインの負担を軽減し、対応の均等化と迅速化を実現します。assignment_v2システムの中核となる実際のアサイン処理を担当します。

**バッチの実行タイミング**：
- PeriodicAssignmentJobから30分毎にトリガーされる
- 会話のステータスがオープンに変更された際にリアルタイムでトリガーされる（AutoAssignmentHandler経由）

**主要な処理内容**：
1. 指定されたinbox_idからインボックスを取得
2. auto_assignment_v2の有効性を確認
3. AssignmentServiceを呼び出して一括アサイン処理を実行
4. 未アサインの会話を優先順位に従って取得
5. 利用可能なエージェントをラウンドロビン方式で選択
6. レート制限を確認し、制限内であればアサインを実行

**前後の処理との関連**：本バッチはPeriodicAssignmentJob（親ジョブ）からキューイングされる子ジョブです。また、会話のステータス変更時にAutoAssignmentHandler経由でリアルタイムにも呼び出されます。

**影響範囲**：指定されたインボックス内の未アサイン会話と、そのインボックスに所属するエージェントが影響を受けます。会話のassignee_idが更新されます。

## バッチ種別

データ更新 / 自動アサイン処理

## 実行スケジュール

| 項目 | 内容 |
|-----|------|
| 実行頻度 | 随時（親ジョブまたはイベントからトリガー） |
| 実行時刻 | 不定 |
| 実行曜日 | 毎日 |
| 実行日 | 毎日 |
| トリガー | PeriodicAssignmentJob / 会話ステータス変更イベント |

## 実行条件

### 前提条件

| 条件 | 説明 |
|-----|------|
| インボックスが存在すること | inbox_idで指定されたインボックスがDBに存在する必要がある |
| auto_assignment_v2が有効であること | インボックスでauto_assignment_v2_enabled?がtrueである必要がある |

### 実行可否判定

- `inbox = Inbox.find_by(id: inbox_id)` でインボックスが見つからない場合は早期リターン
- `inbox.auto_assignment_v2_enabled?` がfalseの場合はAssignmentServiceが0を返す

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | デフォルト値 | 説明 |
|-------------|-----|-----|-------------|------|
| inbox_id | Integer | Yes | - | 処理対象のインボックスID |

### 入力データソース

| データソース | 形式 | 説明 |
|-------------|------|------|
| inboxes | DB | 指定されたインボックスレコード |
| conversations | DB | インボックス内の未アサイン・オープン会話 |
| inbox_members | DB | インボックスに所属するエージェント |
| assignment_policies | DB | アサインポリシー設定 |

## 出力仕様

### 出力データ

| 出力先 | 形式 | 説明 |
|-------|------|------|
| conversations | DB | assignee_idが更新される |
| Railsログ | ログ | アサイン件数がINFOレベルで出力される |

### 出力ファイル仕様

ファイル出力はありません。

## 処理フロー

### 処理シーケンス

```
1. バッチ開始
   └─ defaultキューから起動
2. インボックス取得
   └─ Inbox.find_by(id: inbox_id)
3. インボックス存在チェック
   └─ 存在しない場合は早期リターン
4. AssignmentService初期化
   └─ AutoAssignment::AssignmentService.new(inbox: inbox)
5. 一括アサイン処理実行
   └─ service.perform_bulk_assignment(limit: bulk_assignment_limit)
6. 未アサイン会話を優先順位で取得
   └─ conversation_priority設定に基づきソート
7. 各会話に対してアサイン可否を判定
   └─ status=='open' && assignee_id.nil?
8. 利用可能エージェントを取得・フィルタ
   └─ レート制限チェック
9. ラウンドロビンでエージェント選択
   └─ RoundRobinSelector使用
10. 会話にエージェントをアサイン
    └─ conversation.update!(assignee: agent)
11. アサインイベント発行
    └─ ASSIGNEE_CHANGEDイベントディスパッチ
12. 処理結果をログ出力
    └─ "Assigned #{assigned_count} conversations for inbox #{inbox.id}"
13. バッチ終了
```

### フローチャート

```mermaid
flowchart TD
    A[バッチ開始] --> B[Inbox.find_by inbox_id]
    B --> C{インボックス存在?}
    C -->|No| Z[リターン]
    C -->|Yes| D[AssignmentService初期化]
    D --> E[perform_bulk_assignment呼び出し]
    E --> F{auto_assignment_v2有効?}
    F -->|No| G[0を返す]
    F -->|Yes| H[未アサイン会話取得]
    H --> I{会話あり?}
    I -->|No| J[assigned_countをログ出力]
    I -->|Yes| K{アサイン可能?}
    K -->|No| H
    K -->|Yes| L[利用可能エージェント取得]
    L --> M{エージェントあり?}
    M -->|No| H
    M -->|Yes| N[ラウンドロビン選択]
    N --> O[会話にアサイン]
    O --> P[イベント発行]
    P --> H
    G --> Z
    J --> Z[バッチ終了]
```

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

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

| 処理 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| インボックス取得 | inboxes | SELECT | inbox_idで1件取得 |
| 未アサイン会話取得 | conversations | SELECT | オープン・未アサインの会話を取得 |
| エージェント取得 | inbox_members, users | SELECT | 利用可能なエージェントを取得 |
| アサイン実行 | conversations | UPDATE | assignee_idを更新 |

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

#### inboxes

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, account_id, auto_assignment_config | id = inbox_id | 1件取得 |

#### conversations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, status, assignee_id, last_activity_at, created_at | status='open', assignee_id=NULL | 優先順位でソート |
| UPDATE | assignee_id | 選択されたエージェントのuser_id | 個別更新 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | StandardError | DB操作失敗時 | エラーログを出力し、テスト環境以外では例外を握りつぶす |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | Sidekiqのデフォルト設定 |
| リトライ間隔 | 指数バックオフ |
| リトライ対象エラー | StandardError（ただしテスト環境では再raise） |

### 障害時対応

エラー発生時はログに記録され、次回のPeriodicAssignmentJob実行（30分後）またはリアルタイムトリガー時に再試行されます。
```ruby
# 手動実行
AutoAssignment::AssignmentJob.perform_now(inbox_id: 123)
```

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

| 項目 | 内容 |
|-----|------|
| トランザクション範囲 | 会話単位（各updateは個別トランザクション） |
| コミットタイミング | 各会話のアサイン完了時 |
| ロールバック条件 | 個別会話のupdate失敗時 |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定処理件数 | 最大100件（bulk_assignment_limit） |
| 目標処理時間 | 数秒〜数十秒 |
| メモリ使用量上限 | limit指定により一定に制御 |

## 排他制御

- 同一inbox_idでの重複実行が発生する可能性あり
- 会話のassignee_id更新は個別トランザクションで行われるため、競合時は後勝ち
- アサイン可否チェック（assignable?）でassignee_id.nil?を確認するため、既にアサイン済みの会話はスキップ

## ログ出力

| ログ種別 | 出力タイミング | 出力内容 |
|---------|--------------|---------|
| 開始ログ | - | 明示的な出力なし |
| 進捗ログ | - | 明示的な出力なし |
| 終了ログ | 処理完了時 | "Assigned #{assigned_count} conversations for inbox #{inbox.id}" |
| エラーログ | エラー発生時 | "Bulk assignment failed for inbox #{inbox_id}: #{e.message}" |

## 監視・アラート

| 監視項目 | 閾値 | アラート先 |
|---------|-----|----------|
| エラー件数 | 1件以上 | Sidekiq監視システム |
| 処理時間 | 60秒以上 | Sidekiq監視システム |

## 備考

- bulk_assignment_limitは環境変数 `AUTO_ASSIGNMENT_BULK_LIMIT` で設定可能（デフォルト: 100）
- conversation_priorityオプションで `longest_waiting` を指定すると、last_activity_atの古い順にアサイン
- レート制限機能（RateLimiter）により、エージェントへの過剰なアサインを防止
- Enterprise版ではAssignmentServiceが拡張され、追加のアサインロジックが利用可能
