# 機能設計書 38-最終確認更新

## 概要

本ドキュメントは、Chatwootにおける会話の最終確認時刻更新機能の設計仕様を記載する。エージェントが会話を閲覧した時刻を記録し、未読メッセージの管理に使用する機能を提供する。

### 本機能の処理概要

**業務上の目的・背景**：カスタマーサポート業務において、エージェントが会話を確認したかどうか、いつ確認したかを追跡することは重要である。この情報は、未読メッセージ数の計算、エージェントへの通知制御、チーム内での対応状況の可視化に使用される。最終確認更新機能により、エージェントが会話を開いた時点で自動的に「確認済み」として記録する。

**機能の利用シーン**：
- エージェントが会話詳細画面を開いた際の自動更新
- 未読メッセージを既読にマークする際
- エージェント間での会話引き継ぎ時の確認状況追跡

**主要な処理内容**：
1. 最終確認時刻の更新：agent_last_seen_at、assignee_last_seen_atを現在時刻に更新
2. スロットリング：高頻度のDB書き込みを防ぐため、1時間以内の重複更新をスキップ（未読メッセージがない場合）
3. 担当者/非担当者の区別：担当者かどうかで更新するカラムが異なる

**関連システム・外部連携**：
- WebSocket：リアルタイムで最終確認時刻の更新を通知

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

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 11 | 会話詳細画面 | 主画面 | 画面表示時の自動更新 |

## 機能種別

データ更新 / タイムスタンプ管理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| account_id | Integer | Yes | アカウントID（URLパラメータ） | 存在チェック |
| id | Integer | Yes | 会話のdisplay_id（URLパラメータ） | 存在チェック |

### 入力データソース

- URLパラメータ: account_id, id (conversation display_id)
- 暗黙的: 現在のユーザー（Current.user）、現在時刻

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| conversation | Object | 会話オブジェクト（partialで出力） |

### 出力先

- JSON形式でHTTPレスポンスとして返却

## 処理フロー

### 処理シーケンス

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

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

3. 未読メッセージチェック
   ├─ 担当者の場合: assignee_unread_messages.any?
   └─ 非担当者の場合: unread_messages.any?

4. スロットリング判定
   ├─ 未読メッセージがある場合: 即座に更新
   └─ 未読メッセージがない場合: should_update_last_seen?で判定
       └─ 1時間以内に更新済みならスキップ

5. 最終確認時刻の更新
   └─ update_last_seen_on_conversation
       └─ update_columns (バリデーション・コールバックをスキップ)
       ├─ agent_last_seen_at = DateTime.now.utc
       └─ assignee_last_seen_at = DateTime.now.utc（担当者の場合）

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

### フローチャート

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

    E -->|Yes| F{未読メッセージあり?}
    E -->|No| G{未読メッセージあり?}

    F -->|Yes| H[即座に更新]
    F -->|No| I{1時間以内に更新済み?}

    G -->|Yes| J[即座に更新]
    G -->|No| K{1時間以内に更新済み?}

    I -->|Yes| L[スキップ]
    I -->|No| M[agent + assignee 更新]

    K -->|Yes| L
    K -->|No| N[agent のみ更新]

    H --> M
    J --> N
    L --> O[レスポンス生成]
    M --> O
    N --> O
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-38-1 | 即時更新 | 未読メッセージがある場合は必ず更新 | 未読メッセージ存在時 |
| BR-38-2 | スロットリング | 未読がなく、1時間以内に更新済みの場合は更新をスキップ | 未読なし & 1時間以内更新 |
| BR-38-3 | 担当者カラム | 担当者の場合はassignee_last_seen_atも更新 | 担当者の場合 |
| BR-38-4 | バリデーションスキップ | update_columnsでバリデーション・コールバックをスキップ | 常時 |

### 計算ロジック

- 担当者判定: `@conversation.assignee_id? && Current.user == @conversation.assignee`
- スロットリング判定: `agent_last_seen_at < 1.hour.ago` または `assignee_last_seen_at < 1.hour.ago`

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| update_last_seen | conversations | UPDATE | agent_last_seen_at, (assignee_last_seen_at) |

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

#### conversations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | agent_last_seen_at | DateTime.now.utc | 常に更新 |
| UPDATE | assignee_last_seen_at | DateTime.now.utc | 担当者の場合のみ |

**注意**: update_columnsを使用しているため、updated_atは更新されない。

## エラー処理

### エラーケース一覧

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

### リトライ仕様

- 単純なDB更新のため、特別なリトライ処理は実装されていない

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

- update_columnsは単一SQL文で実行されるため、アトミック性が保証される
- トランザクションは使用しない

## パフォーマンス要件

- スロットリングにより、高頻度アクセス時のDB書き込みを1時間に1回に制限
- update_columnsによりバリデーション・コールバックをスキップし、高速化

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

- ConversationPolicy#show?による会話アクセス権限チェック
- 時刻は常にサーバー側で生成（クライアント指定不可）

## 備考

- update_columnsを使用しているため、updated_atコールバックは発動しない
- 未読メッセージの計算は、agent_last_seen_at以降に作成されたメッセージをカウント
- フロントエンドは会話詳細画面を開いた際に自動的にこのAPIを呼び出す

---

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

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

### 推奨読解順序

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

まず、最終確認時刻に関連するカラムを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | conversation.rb | `app/models/conversation.rb` | last_seen_at関連カラムと未読メッセージメソッド |

**読解のコツ**: agent_last_seen_at（全エージェント用）とassignee_last_seen_at（担当者用）の2つのカラムがある。

**主要処理フロー**:
- **7-8行目**: agent_last_seen_at, assignee_last_seen_at カラム（スキーマコメント）
- **166-168行目**: unread_messages - agent_last_seen_at以降のメッセージ
- **170-172行目**: assignee_unread_messages - assignee_last_seen_at以降のメッセージ

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

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

**主要処理フロー**:
- **112-123行目**: update_last_seen
  - 担当者 & 未読あり → 即時更新（assignee含む）
  - 非担当者 & 未読あり → 即時更新（agentのみ）
  - should_update_last_seen?でスロットリング判定
- **153-160行目**: update_last_seen_on_conversation
  - update_columnsでバリデーション・コールバックをスキップ
  - 担当者フラグによりassignee_last_seen_atの更新を制御
- **162-171行目**: should_update_last_seen?
  - 1時間以内に更新済みかチェック
  - 担当者/非担当者で判定ロジックが異なる
- **228-230行目**: assignee?
  - 担当者かどうかの判定メソッド

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

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

**主要処理フロー**:
- **1行目**: 会話パーシャルを使用してJSON生成

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

```
Api::V1::Accounts::ConversationsController#update_last_seen
    │
    ├─ conversation (before_action)
    │      └─ Current.account.conversations.find_by!
    │      └─ authorize @conversation, :show?
    │
    ├─ assignee?
    │      └─ @conversation.assignee_id? && Current.user == assignee
    │
    ├─ 【担当者 + 未読あり】
    │      └─ assignee_unread_messages.any?
    │             └─ update_last_seen_on_conversation(now, true)
    │
    ├─ 【非担当者 + 未読あり】
    │      └─ unread_messages.any?
    │             └─ update_last_seen_on_conversation(now, false)
    │
    └─ 【未読なし】
           └─ should_update_last_seen?
                  ├─ agent_last_seen_at < 1.hour.ago
                  └─ assignee_last_seen_at < 1.hour.ago (担当者)
                         └─ update_last_seen_on_conversation
                                └─ update_columns
                                       ├─ agent_last_seen_at
                                       └─ assignee_last_seen_at (担当者)
```

### データフロー図

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

conversation_id ───▶ ConversationsController  ───▶ JSON Response
                            │                          (conversation)
                            ▼
                    assignee? チェック
                            │
                    ┌───────┴───────┐
                    ▼               ▼
            担当者            非担当者
                    │               │
                    ▼               ▼
        assignee_unread    unread_messages
            .any?               .any?
                    │               │
                    └───────┬───────┘
                            ▼
                should_update_last_seen?
                            │
                            ▼
                    update_columns
                            │
                            ▼
                    conversations
                    (DB Update)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| conversations_controller.rb | `app/controllers/api/v1/accounts/conversations_controller.rb` | コントローラー | update_last_seen（112-123行目）、関連メソッド |
| conversation.rb | `app/models/conversation.rb` | モデル | unread_messages、assignee_unread_messages |
| update_last_seen.json.jbuilder | `app/views/api/v1/accounts/conversations/update_last_seen.json.jbuilder` | ビュー | レスポンスJSON生成 |
| conversation_policy.rb | `app/policies/conversation_policy.rb` | ポリシー | アクセス権限制御 |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義（151行目） |
