# 機能設計書 37-スヌーズ

## 概要

本ドキュメントは、Chatwootにおける会話スヌーズ機能の設計仕様を記載する。会話を一時的に「スヌーズ」状態にし、指定した時刻まで非表示にする機能を提供する。

### 本機能の処理概要

**業務上の目的・背景**：カスタマーサポート業務において、すぐに対応できないが後で対応が必要な会話がある。例えば、顧客からの追加情報待ち、内部確認待ち、特定の時間帯に対応したい場合などである。スヌーズ機能により、これらの会話を一時的にオープン会話リストから非表示にし、指定時刻に自動的に再表示させることで、エージェントのワークフローを効率化する。

**機能の利用シーン**：
- 顧客からの返信を待っている会話を一時的に非表示にする
- 翌営業日に対応したい会話をスヌーズする
- 特定の時間帯（例: 午後）に対応したい会話をスヌーズする
- 社内確認が完了するまで会話を一時的に保留する

**主要な処理内容**：
1. スヌーズ状態への変更：toggle_statusアクションでstatusを「snoozed」に変更
2. スヌーズ解除時刻の設定：snoozed_untilに指定時刻を設定
3. 自動再開：スヌーズ期限が過ぎた会話は自動的に「open」に戻る（別機能で実装）

**関連システム・外部連携**：
- WebSocket：リアルタイムでステータス変更を通知
- Sidekiq：スヌーズ解除のバックグラウンドジョブ（別機能）

**権限による制御**：
- 会話へのアクセス権限（ConversationPolicy#show?）を持つユーザーのみがスヌーズ操作が可能

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 11 | 会話詳細画面 | 主画面 | スヌーズボタンの操作、時刻指定 |

## 機能種別

ステータス管理 / データ更新

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| account_id | Integer | Yes | アカウントID（URLパラメータ） | 存在チェック |
| id | Integer | Yes | 会話のdisplay_id（URLパラメータ） | 存在チェック |
| status | String | Yes | ステータス（"snoozed"） | enum値チェック |
| snoozed_until | DateTime | Yes（スヌーズ時） | スヌーズ解除日時 | 有効な日時形式 |

### 入力データソース

- URLパラメータ: account_id, id (conversation display_id)
- リクエストボディ: status, snoozed_until

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| meta | Object | 空オブジェクト |
| payload.success | Boolean | 処理成功フラグ |
| payload.conversation_id | Integer | 会話のdisplay_id |
| payload.current_status | String | 現在のステータス（"snoozed"） |
| payload.snoozed_until | DateTime | スヌーズ解除日時 |

### 出力先

- JSON形式でHTTPレスポンスとして返却
- WebSocket経由でリアルタイム通知

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信
   └─ account_id, conversation_idの検証

2. 会話取得・認可チェック
   └─ ConversationPolicy#show?

3. ステータス変更処理
   └─ set_conversation_status
       ├─ @conversation.status = 'snoozed'
       └─ @conversation.snoozed_until = parse_date_time(params[:snoozed_until])

4. 会話保存
   └─ @conversation.save!

5. アサイン処理（該当する場合）
   └─ should_assign_conversation? → assign_conversation

6. レスポンス生成
   └─ toggle_status.json.jbuilder
```

### フローチャート

```mermaid
flowchart TD
    A[リクエスト受信] --> B[会話取得]
    B --> C{認可チェック}
    C -->|失敗| D[403 Forbidden]
    C -->|成功| E{status=snoozed?}

    E -->|Yes| F[set_conversation_status]
    F --> G[status = snoozed]
    G --> H[snoozed_until設定]
    H --> I[save!]

    E -->|No| J[他のステータス処理]
    J --> I

    I --> K{openになった?}
    K -->|Yes| L{エージェントユーザー?}
    K -->|No| M[レスポンス生成]

    L -->|Yes| N[assign_conversation]
    L -->|No| M
    N --> M
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-37-1 | スヌーズ時刻必須 | status='snoozed'の場合、snoozed_untilパラメータが必要 | スヌーズ設定時 |
| BR-37-2 | スヌーズ解除時リセット | ステータスがsnoozed以外に変更された場合、snoozed_untilはNULLにリセットされる | ステータス変更時 |
| BR-37-3 | 自動アサイン | ステータスがopenになった場合、現在のエージェントユーザーが自動的にアサインされる | open変更時 |
| BR-37-4 | アクティビティ記録 | ステータス変更はアクティビティメッセージとして記録される | ステータス変更時 |

### 計算ロジック

- スヌーズ解除日時のパース: `parse_date_time(params[:snoozed_until].to_s)`

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| toggle_status | conversations | UPDATE | status = 'snoozed', snoozed_until設定 |
| toggle_status | messages | INSERT | アクティビティメッセージ（非同期Job） |

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

#### conversations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | status | 3 (snoozed) | enum値 |
| UPDATE | snoozed_until | 指定された日時 | スヌーズ解除時刻 |
| UPDATE | updated_at | 現在時刻 | |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 | NotFound | 会話が存在しない | 正しいconversation_idを指定 |
| 403 | Forbidden | 会話へのアクセス権限がない | 権限を持つユーザーでアクセス |
| 422 | UnprocessableEntity | 無効な日時形式 | 有効な日時形式を指定 |

### リトライ仕様

- アクティビティメッセージ作成はJobとして非同期実行されるため、失敗時はSidekiqのリトライ機構に従う

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

- 会話のsave!は単一トランザクションで実行
- アクティビティメッセージは別トランザクション（非同期Job）

## パフォーマンス要件

- 単純なDB更新操作のため、特別なパフォーマンス要件なし

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

- ConversationPolicy#show?による会話アクセス権限チェック
- 日時パラメータのパース処理でのエラーハンドリング

## 備考

- スヌーズ状態の会話は通常のオープン会話リストには表示されない
- スヌーズされた会話への新着メッセージは通知される可能性がある
- スヌーズ解除時刻が過ぎた会話の自動再開は別のバックグラウンドジョブで処理される

---

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

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

### 推奨読解順序

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

まず、会話のスヌーズ関連のカラムを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | conversation.rb | `app/models/conversation.rb` | スヌーズ関連のカラムとenum定義 |

**読解のコツ**: statusはenumで定義（open: 0, resolved: 1, pending: 2, snoozed: 3）。snoozed_untilはdatetime型。

**主要処理フロー**:
- **16-17行目**: snoozed_until カラム定義（スキーマコメント）
- **75行目**: enum status - snoozed: 3
- **117行目**: before_save :ensure_snooze_until_reset
- **236-238行目**: ensure_snooze_until_reset - スヌーズ以外ならNULLに

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | conversations_controller.rb | `app/controllers/api/v1/accounts/conversations_controller.rb` | toggle_statusアクションの実装 |

**主要処理フロー**:
- **78-89行目**: toggle_status
  - pending_to_open_by_bot?のチェック
  - params[:status]があればset_conversation_status
  - 会話を保存
  - should_assign_conversation?ならassign_conversation
- **173-176行目**: set_conversation_status
  - @conversation.status = params[:status]
  - snoozed_untilパラメータのパース

#### Step 3: ビューを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | toggle_status.json.jbuilder | `app/views/api/v1/accounts/conversations/toggle_status.json.jbuilder` | レスポンスJSON形式 |

**主要処理フロー**:
- **1-2行目**: meta（空オブジェクト）
- **4-9行目**: payload
  - success: @status
  - conversation_id: display_id
  - current_status: status
  - snoozed_until: snoozed_until

#### Step 4: スヌーズリセットを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | conversation.rb | `app/models/conversation.rb` | ensure_snooze_until_resetコールバック |

**主要処理フロー**:
- **236-238行目**: ensure_snooze_until_reset
  - snoozed?でない場合、snoozed_untilをnilに設定
  - before_saveコールバックで自動実行

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

```
Api::V1::Accounts::ConversationsController#toggle_status
    │
    ├─ conversation (before_action)
    │      └─ Current.account.conversations.find_by!
    │      └─ authorize @conversation, :show?
    │
    ├─ pending_to_open_by_bot?
    │      └─ Current.user.is_a?(AgentBot) && pending → open
    │
    ├─ set_conversation_status
    │      ├─ @conversation.status = params[:status]
    │      └─ @conversation.snoozed_until = parse_date_time(snoozed_until)
    │
    ├─ @conversation.save!
    │      └─ before_save :ensure_snooze_until_reset
    │             └─ snoozed_until = nil (if not snoozed?)
    │
    └─ should_assign_conversation?
           └─ assign_conversation (if open && agent)
                  └─ @conversation.assignee = current_user
```

### データフロー図

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

conversation_id ───▶ ConversationsController  ───▶ JSON Response
status='snoozed'          │                          (payload)
snoozed_until             │
                          ▼
                set_conversation_status
                          │
                    ┌─────┴─────┐
                    ▼           ▼
                status     snoozed_until
                (snoozed)  (DateTime)
                          │
                          ▼
                    save!
                          │
                    ┌─────┴─────┐
                    ▼           ▼
              conversations  Dispatcher
              (DB Update)    (WebSocket)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| conversations_controller.rb | `app/controllers/api/v1/accounts/conversations_controller.rb` | コントローラー | toggle_status（78-89行目）、set_conversation_status（173-176行目） |
| conversation.rb | `app/models/conversation.rb` | モデル | スヌーズカラム、ensure_snooze_until_reset |
| toggle_status.json.jbuilder | `app/views/api/v1/accounts/conversations/toggle_status.json.jbuilder` | ビュー | レスポンスJSON生成 |
| conversation_policy.rb | `app/policies/conversation_policy.rb` | ポリシー | アクセス権限制御 |
| date_range_helper.rb | `app/helpers/date_range_helper.rb` | ヘルパー | parse_date_time |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義（148行目） |
