# 機能設計書 187-バルクアクション

## 概要

本ドキュメントは、Chatwootにおける「バルクアクション」機能の設計仕様を定義する。本機能は、複数の会話に対して一括で操作（ステータス変更、担当者変更、ラベル付与など）を行うための機能である。

### 本機能の処理概要

本機能は、複数の会話IDを指定して、一括でステータス変更、担当者変更、チーム変更、ラベルの追加・削除、スヌーズ設定などの操作を行う。バックグラウンドジョブとして非同期で実行されるため、大量の会話を処理する場合でもUIがブロックされない。

**業務上の目的・背景**：カスタマーサポート業務において、朝一のチェックで前日からの未対応会話を一括で担当者に割り当てたり、特定の条件に合う会話を一括でクローズしたりするニーズがある。1件ずつ操作すると時間がかかるため、バルク操作により業務効率を大幅に向上させる。

**機能の利用シーン**：シフト交代時に、前の担当者の会話を新しい担当者に一括で引き継ぐ。キャンペーン終了時に関連する会話を一括でクローズする。特定のラベルを複数の会話に一括で付与する。

**主要な処理内容**：
1. 会話IDリストの受信
2. 各会話に対するフィールド更新（status, assignee_id, team_id）
3. ラベルの追加・削除
4. スヌーズ期間の設定

**関連システム・外部連携**：Sidekiqによる非同期ジョブ処理

**権限による制御**：会話の更新権限を持つエージェント以上が操作可能

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 9 | ダッシュボードホーム画面 | 主画面 | 会話一覧からの複数選択と一括操作 |
| 10 | インボックス別会話一覧 | 主画面 | インボックス内会話の一括操作 |

## 機能種別

バッチ処理 / 非同期処理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| type | String | Yes | 対象タイプ | "Conversation" または "conversation" |
| ids | Array[Integer] | Yes | 会話のdisplay_idリスト | 配列 |
| fields | Object | No | 更新するフィールド | |
| fields.status | String | No | ステータス | open, resolved, pending, snoozed |
| fields.assignee_id | Integer | No | 担当者ID | null許容 |
| fields.team_id | Integer | No | チームID | null許容 |
| labels | Object | No | ラベル操作 | |
| labels.add | Array[String] | No | 追加するラベル | 文字列配列 |
| labels.remove | Array[String] | No | 削除するラベル | 文字列配列 |
| snoozed_until | String | No | スヌーズ終了日時 | ISO8601形式 |

### 入力データソース

- APIリクエスト: POST /api/v1/accounts/{account_id}/bulk_actions

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| HTTP Status | Integer | 200 OK（ジョブエンキュー成功時） |

### 出力先

- HTTPレスポンス（head :ok）
- 会話データの更新

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信
   └─ BulkActionsController#create

2. タイプ判定
   └─ normalized_typeで "Conversation" を判定

3. ジョブエンキュー
   └─ BulkActionsJob.perform_laterを呼び出し

4. レスポンス返却
   └─ head :ok

5. バックグラウンド処理
   ├─ 対象会話の取得（display_idで検索）
   ├─ ラベル削除処理
   ├─ 各会話に対する更新処理
   │      ├─ ラベル追加
   │      ├─ スヌーズ設定
   │      └─ フィールド更新
   └─ 完了
```

### フローチャート

```mermaid
flowchart TD
    A[リクエスト受信] --> B{type判定}
    B -->|Conversation| C[enqueue_conversation_job]
    B -->|Contact| D[enqueue_contact_job]
    B -->|その他| E[422 エラー]

    C --> F[BulkActionsJob.perform_later]
    F --> G[head :ok]

    subgraph Background Job
        H[records_to_updated] --> I[bulk_remove_labels]
        I --> J[bulk_conversation_update]
        J --> K[各会話ループ]
        K --> L[bulk_add_labels]
        K --> M[bulk_snoozed_until]
        K --> N[conversation.update]
    end

    G --> H
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-187-01 | 対象タイプ制限 | type は "Conversation" または "Contact" のみ | 常時 |
| BR-187-02 | display_id検索 | 会話はidではなくdisplay_idで検索 | 会話操作時 |
| BR-187-03 | nullフィールドのスキップ | statusがnullの場合はフィールドから除外 | 更新時 |
| BR-187-04 | ラベル削除優先 | 削除処理は追加処理より先に実行 | ラベル操作時 |

### 計算ロジック

スヌーズ期間の計算:
- `snoozed_until` をparse_date_timeで日時に変換
- DateRangeHelperのロジックを使用

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 会話取得 | conversations | SELECT | display_idで対象会話を取得 |
| ステータス更新 | conversations | UPDATE | status, assignee_id, team_id等 |
| ラベル更新 | labels, taggings | UPDATE | ラベルの追加・削除 |

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

#### conversations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | * | account_id = @account.id, display_id IN (ids) | |
| UPDATE | status | リクエストパラメータ | |
| UPDATE | assignee_id | リクエストパラメータ | null許容 |
| UPDATE | team_id | リクエストパラメータ | null許容 |
| UPDATE | snoozed_until | リクエストパラメータ（日時） | |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 422 | Unprocessable Entity | 不正なtype指定 | エラーレスポンス返却 |
| 500 | Internal Server Error | ジョブ実行エラー | Sidekiqリトライ |

### リトライ仕様

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

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

各会話の更新は個別のトランザクションで実行。1件の失敗が他に影響しない。

## パフォーマンス要件

- ジョブはmediumキューで実行
- 大量の会話でもUIはブロックされない
- 即座にhead :okを返却

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

- アカウント境界の厳守（account_idでスコープ）
- Current.userによる監査情報の記録

## 備考

- display_idとidは異なることに注意
- Contact向けの処理は別の機能（No.188）で詳述

---

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

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

### 推奨読解順序

#### Step 1: コントローラーを理解する

まず、APIエントリーポイントを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | bulk_actions_controller.rb | `app/controllers/api/v1/accounts/bulk_actions_controller.rb` | APIエントリーポイント |

**読解のコツ**:
- **2-14行目**: createアクション - タイプ判定とジョブエンキュー
- **18行目**: normalized_type - パラメータの正規化
- **22-28行目**: enqueue_conversation_job - ジョブ呼び出し
- **46-54行目**: conversation_params - 会話用パラメータ

#### Step 2: バックグラウンドジョブを理解する

非同期処理の実装を理解する。

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

**主要処理フロー**:
- **9-17行目**: performメソッド - ジョブのメイン処理
- **19-22行目**: bulk_update - 更新処理の呼び出し
- **24-31行目**: bulk_conversation_update - 各会話への更新適用
- **33-37行目**: bulk_remove_labels - ラベル削除処理
- **45-47行目**: bulk_add_labels - ラベル追加処理
- **60-65行目**: records_to_updated - 対象レコードの取得

#### Step 3: 日付処理を理解する

スヌーズ日時の解析を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | date_range_helper.rb | `app/helpers/date_range_helper.rb` | 日付解析ヘルパー |

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

```
POST /api/v1/accounts/:account_id/bulk_actions
    │
    └─ BulkActionsController#create
           │
           ├─ normalized_type ("Conversation" 判定)
           │
           └─ enqueue_conversation_job
                  │
                  └─ BulkActionsJob.perform_later
                         │
                         ├─ records_to_updated
                         │      └─ Conversation.where(account_id:, display_id:)
                         │
                         ├─ bulk_remove_labels
                         │      └─ conversation.update(label_list:)
                         │
                         └─ bulk_conversation_update
                                ├─ bulk_add_labels
                                │      └─ conversation.add_labels
                                ├─ bulk_snoozed_until
                                │      └─ parse_date_time
                                └─ conversation.update(params)
```

### データフロー図

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

APIリクエスト ─────────▶ BulkActionsController ─────────▶ head :ok
(ids[], fields, labels)        │
                               ▼
                       BulkActionsJob (非同期)
                               │
                               ▼
                       対象会話取得
                       (display_idで検索)
                               │
                               ▼
                       各会話を更新
                       (status, assignee_id, labels...)
                               │
                               ▼
                       conversations テーブル
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| bulk_actions_controller.rb | `app/controllers/api/v1/accounts/bulk_actions_controller.rb` | コントローラー | APIエントリーポイント |
| bulk_actions_job.rb | `app/jobs/bulk_actions_job.rb` | ジョブ | バルク更新処理 |
| conversation.rb | `app/models/conversation.rb` | モデル | 会話モデル（label関連メソッド） |
| date_range_helper.rb | `app/helpers/date_range_helper.rb` | ヘルパー | 日付解析 |
