# 機能設計書 36-自動解決

## 概要

本ドキュメントは、Chatwootにおける会話の自動解決機能の設計仕様を記載する。一定時間アクティビティがない会話を自動的に「解決済み」ステータスに変更するバックグラウンド処理を提供する。

### 本機能の処理概要

**業務上の目的・背景**：カスタマーサポート業務において、顧客からの応答がなく放置された会話が「オープン」状態のまま残ることがある。これらの会話がエージェントの作業キューに残り続けると、本当に対応が必要な会話の発見が困難になる。自動解決機能により、設定した時間が経過した非アクティブな会話を自動的にクローズし、サポートチームのワークフローを効率化する。

**機能の利用シーン**：
- 顧客が問い合わせ後に応答しなくなった会話を自動的にクローズする
- 夜間や休日に放置された会話を営業時間開始前に整理する
- オープン会話の数を管理可能な範囲に保つ
- SLA要件に基づいた会話の自動クローズ

**主要な処理内容**：
1. スケジューラージョブ：定期的に全アカウントをチェックし、自動解決対象アカウントを抽出
2. 解決ジョブ：各アカウントの対象会話を抽出し、ステータスを「resolved」に変更
3. オプション処理：自動解決メッセージの送信、自動ラベル付与

**関連システム・外部連携**：
- Sidekiq：バックグラウンドジョブの実行
- Cron / スケジューラー：定期実行のトリガー

**権限による制御**：
- アカウント設定（auto_resolve_after）による有効化・無効化
- 処理はシステムジョブとして実行され、ユーザー権限は関係しない

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 45 | 一般設定画面 | 設定画面 | 自動解決の時間設定 |

## 機能種別

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

## 入力仕様

### 入力パラメータ（アカウント設定）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| auto_resolve_after | Integer | No | 自動解決までの分数 | 10〜1,439,856（約1000日） |
| auto_resolve_message | String | No | 自動解決時に送信するメッセージ | - |
| auto_resolve_ignore_waiting | Boolean | No | 顧客待ち状態の会話を除外するか | true/false |
| auto_resolve_label | String | No | 自動解決時に付与するラベル | - |

### 入力データソース

- アカウント設定（settings JSONカラム）
- 会話テーブル（last_activity_at, waiting_since, status）

## 出力仕様

### 出力データ

本機能はバックグラウンドジョブのため、直接的なHTTPレスポンスはない。

### 出力先

- conversations テーブル: status = 'resolved' に更新
- messages テーブル: 自動解決メッセージ（設定時のみ）
- アクティビティメッセージ: 自動解決のシステムメッセージ

## 処理フロー

### 処理シーケンス

```
【ConversationsResolutionSchedulerJob - 定期実行】
1. 定期実行トリガー（Cron）
   └─ scheduled_jobs キューで実行

2. 自動解決設定のあるアカウントを抽出
   └─ Account.with_auto_resolve
   └─ バッチ処理（100件ずつ）

3. 各アカウントのResolutionJobを起動
   └─ Conversations::ResolutionJob.perform_later(account:)

【Conversations::ResolutionJob - 解決処理】
1. 対象会話の抽出
   ├─ auto_resolve_ignore_waiting = true の場合
   │      └─ resolvable_not_waiting: waiting_sinceがNULLの会話
   └─ auto_resolve_ignore_waiting = false の場合
          └─ resolvable_all: 全てのオープン会話

2. 会話のフィルタリング
   └─ last_activity_at < (現在時刻 - auto_resolve_after分)
   └─ status = 'open'
   └─ contact_id IS NOT NULL（孤児会話を除外）
   └─ LIMIT 500件

3. 各会話の処理
   ├─ 自動解決メッセージ送信（設定時）
   │      └─ MessageTemplates::Template::AutoResolve
   ├─ ラベル付与（設定時）
   │      └─ conversation.add_labels(auto_resolve_label)
   └─ ステータス変更
          └─ conversation.toggle_status (open → resolved)
```

### フローチャート

```mermaid
flowchart TD
    A[Cron トリガー] --> B[SchedulerJob開始]
    B --> C[auto_resolve設定のあるアカウントを抽出]
    C --> D{アカウントあり?}
    D -->|No| E[終了]
    D -->|Yes| F[ResolutionJob起動]

    F --> G{ignore_waiting?}
    G -->|Yes| H[waiting_since NULLの会話抽出]
    G -->|No| I[全オープン会話抽出]
    H --> J[last_activity_atでフィルタ]
    I --> J

    J --> K{対象会話あり?}
    K -->|No| E
    K -->|Yes| L{auto_resolve_message設定?}

    L -->|Yes| M[自動解決メッセージ送信]
    L -->|No| N{auto_resolve_label設定?}
    M --> N

    N -->|Yes| O[ラベル付与]
    N -->|No| P[toggle_status実行]
    O --> P

    P --> Q[次の会話へ]
    Q --> K
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-36-1 | 最小解決時間 | auto_resolve_afterは最小10分 | 設定時 |
| BR-36-2 | 最大解決時間 | auto_resolve_afterは最大1,439,856分（約1000日） | 設定時 |
| BR-36-3 | 待機状態除外オプション | auto_resolve_ignore_waitingがtrueの場合、waiting_sinceがNULLでない会話は除外 | ジョブ実行時 |
| BR-36-4 | バッチ制限 | 1回のジョブで最大500件の会話を処理 | ジョブ実行時 |
| BR-36-5 | 孤児会話除外 | contact_idがNULLの会話は処理対象外 | ジョブ実行時 |
| BR-36-6 | メッセージウィンドウチェック | 自動解決メッセージはメッセージング可能な場合のみ送信 | メッセージ送信時 |

### 計算ロジック

- 対象会話: `last_activity_at < (Time.now.utc - auto_resolve_after.minutes)`
- アクティビティメッセージ: 分→時間→日に変換して表示（例: "60分" → "1時間"）

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| SchedulerJob | accounts | SELECT | auto_resolve設定のあるアカウント取得 |
| ResolutionJob | conversations | SELECT | 対象会話の抽出 |
| ResolutionJob | conversations | UPDATE | status = 'resolved' |
| ResolutionJob | messages | INSERT | 自動解決メッセージ（設定時） |
| ResolutionJob | taggings | INSERT | ラベル付与（設定時） |

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

#### conversations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | status, last_activity_at, waiting_since | status = 'open' AND last_activity_at < 期限 | |
| UPDATE | status | 'resolved' (1) | toggle_statusで更新 |
| UPDATE | updated_at | 現在時刻 | |

#### messages（自動解決メッセージ）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | account_id | 会話のaccount_id | |
| INSERT | inbox_id | 会話のinbox_id | |
| INSERT | message_type | 'template' | |
| INSERT | content | account.auto_resolve_message | |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | JobError | ジョブ実行中の例外 | Sidekiqのリトライ機構で再試行 |
| - | MessageWindowError | メッセージング不可 | アクティビティメッセージとしてログ記録 |

### リトライ仕様

- Sidekiqの標準リトライ機構に従う
- 各会話の処理は独立しているため、1件の失敗が他に影響しない

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

- 各会話の処理は個別に実行される（会話単位でのアトミック性）
- 大量の会話を一括更新するトランザクションは使用しない

## パフォーマンス要件

- SchedulerJob: low キューで実行
- バッチ処理: 100アカウントずつ処理
- 会話処理: 500件制限で負荷を抑制
- インデックス: last_activity_at, status にインデックスあり

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

- システムジョブとして実行されるため、ユーザー認証は不要
- Current.userは設定されないため、アクティビティメッセージは「システム」として記録される

## 備考

- 自動解決設定はアカウント単位
- メッセージング期間外の場合、自動解決メッセージの代わりにアクティビティメッセージが記録される
- スヌーズ中の会話も対象になる可能性がある（statusがopenではないため通常は対象外）

---

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

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

### 推奨読解順序

#### Step 1: アカウント設定を理解する

まず、自動解決に関連するアカウント設定を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | account.rb | `app/models/account.rb` | auto_resolve_after等の設定項目 |

**読解のコツ**: settingsはJSONBカラムでstore_accessorを使用。SETTINGS_PARAMS_SCHEMAでバリデーション。

**主要処理フロー**:
- **33-73行目**: SETTINGS_PARAMS_SCHEMA - 設定のJSONスキーマ定義
  - auto_resolve_after: 10〜1,439,856の整数
- **86-89行目**: store_accessor - 設定項目のアクセサ定義
- **139行目**: scope :with_auto_resolve - 自動解決設定のあるアカウント

#### Step 2: スケジューラージョブを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | conversations_resolution_scheduler_job.rb | `app/jobs/account/conversations_resolution_scheduler_job.rb` | 定期実行ジョブ |

**主要処理フロー**:
- **2行目**: queue_as :scheduled_jobs - スケジュールジョブキュー
- **4-7行目**: perform
  - Account.with_auto_resolveで対象アカウント取得
  - find_each(batch_size: 100)でバッチ処理
  - ResolutionJob.perform_laterで各アカウントのジョブ起動

#### Step 3: 解決ジョブを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | resolution_job.rb | `app/jobs/conversations/resolution_job.rb` | 会話解決処理 |

**主要処理フロー**:
- **2行目**: queue_as :low - 低優先度キュー
- **4-13行目**: perform
  - conversation_scope()で対象会話抽出
  - limit(500)でバッチ制限
  - AutoResolve.perform（メッセージ送信）
  - add_labels（ラベル付与）
  - toggle_status（ステータス変更）
- **17-26行目**: conversation_scope
  - auto_resolve_ignore_waitingによる条件分岐
  - contact_id IS NOT NULLで孤児会話除外

#### Step 4: 自動解決メッセージを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | auto_resolve.rb | `app/services/message_templates/template/auto_resolve.rb` | 自動解決メッセージ |

**主要処理フロー**:
- **4-11行目**: perform
  - auto_resolve_messageが空なら終了
  - within_messaging_window?でメッセージング可能かチェック
  - 可能ならメッセージ作成、不可ならアクティビティメッセージ
- **19-21行目**: within_messaging_window? - can_reply?の呼び出し

#### Step 5: 会話スコープを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | conversation.rb | `app/models/conversation.rb` | resolvable_*スコープ |

**主要処理フロー**:
- **82-86行目**: resolvable_not_waiting
  - openステータス
  - last_activity_at < 期限
  - waiting_since IS NULL
- **87-91行目**: resolvable_all
  - openステータス
  - last_activity_at < 期限

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

```
[Cron] → Account::ConversationsResolutionSchedulerJob
              │
              ├─ Account.with_auto_resolve
              │      └─ settings ->> 'auto_resolve_after' IS NOT NULL
              │
              └─ find_each (batch_size: 100)
                     │
                     └─ Conversations::ResolutionJob.perform_later(account:)
                            │
                            ├─ conversation_scope(account)
                            │      ├─ resolvable_not_waiting (if ignore_waiting)
                            │      └─ resolvable_all (if not ignore_waiting)
                            │
                            └─ resolvable_conversations.each
                                   │
                                   ├─ MessageTemplates::Template::AutoResolve
                                   │      ├─ can_reply?
                                   │      └─ messages.create! or ActivityMessageJob
                                   │
                                   ├─ conversation.add_labels
                                   │
                                   └─ conversation.toggle_status
                                          └─ status: open → resolved
```

### データフロー図

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

Account Settings  ───▶ SchedulerJob        ───▶ ResolutionJob起動
(auto_resolve_*)          │
                          ▼
                    Account.with_auto_resolve
                          │
                          ▼
Conversations     ───▶ ResolutionJob       ───▶ conversations
(open, stale)             │                     (resolved)
                          │
                    ┌─────┴─────┐
                    ▼           ▼
              AutoResolve   toggle_status
                    │           │
                    ▼           ▼
                messages    conversations
                (template)  (status update)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| conversations_resolution_scheduler_job.rb | `app/jobs/account/conversations_resolution_scheduler_job.rb` | ジョブ | 定期実行スケジューラー |
| resolution_job.rb | `app/jobs/conversations/resolution_job.rb` | ジョブ | 会話解決処理 |
| auto_resolve.rb | `app/services/message_templates/template/auto_resolve.rb` | サービス | 自動解決メッセージ送信 |
| account.rb | `app/models/account.rb` | モデル | 設定管理（auto_resolve_*） |
| conversation.rb | `app/models/conversation.rb` | モデル | resolvable_*スコープ |
| activity_message_handler.rb | `app/models/concerns/activity_message_handler.rb` | モジュール | アクティビティメッセージ生成 |
