# 機能設計書 40-未読マーク

## 概要

本ドキュメントは、Chatwootにおける会話の未読マーク機能の設計仕様を記載する。既読にした会話を意図的に「未読」状態に戻し、後で再度対応するためにマークする機能を提供する。

### 本機能の処理概要

**業務上の目的・背景**：カスタマーサポート業務において、エージェントが会話を確認したが、すぐに対応できない場合がある。そのような場合に、会話を「未読」としてマークしておくことで、後で対応が必要な会話を識別しやすくする。これは一般的なメールクライアントの「未読にする」機能と同様の操作性を提供する。

**機能の利用シーン**：
- 会話を確認したが、後で詳しく対応したい場合
- 複数の会話を確認した後、優先度の高いものを未読に戻して目立たせる場合
- 一時的に他の作業に移る際に、戻るべき会話をマークする場合

**主要な処理内容**：
1. 最終確認時刻のリセット：最後の受信メッセージより1秒前に戻すことで、未読状態を作成
2. 担当者の最終確認時刻も更新：assignee_last_seen_atも同様に更新

**関連システム・外部連携**：
- WebSocket：リアルタイムで会話情報の更新を通知

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

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 11 | 会話詳細画面 | 主画面 | 未読マークボタンの操作 |

## 機能種別

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

## 入力仕様

### 入力パラメータ

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

### 入力データソース

- URLパラメータ: account_id, id (conversation display_id)

## 出力仕様

### 出力データ

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

### 出力先

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

## 処理フロー

### 処理シーケンス

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

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

3. 最後の受信メッセージ取得
   └─ @conversation.messages.incoming.last

4. 最終確認時刻の計算
   └─ last_incoming_message.created_at - 1.second
   └─ メッセージがない場合はnil

5. 最終確認時刻の更新
   └─ update_last_seen_on_conversation(last_seen_at, true)
       └─ update_columns
           ├─ agent_last_seen_at = last_seen_at
           └─ assignee_last_seen_at = last_seen_at

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

### フローチャート

```mermaid
flowchart TD
    A[リクエスト受信] --> B[会話取得]
    B --> C{認可チェック}
    C -->|失敗| D[403 Forbidden]
    C -->|成功| E[最後の受信メッセージ取得]

    E --> F{受信メッセージあり?}
    F -->|No| G[last_seen_at = nil]
    F -->|Yes| H[last_seen_at = created_at - 1秒]

    G --> I[update_last_seen_on_conversation]
    H --> I

    I --> J[update_columns]
    J --> K[agent_last_seen_at更新]
    K --> L[assignee_last_seen_at更新]
    L --> M[レスポンス生成]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-40-1 | 1秒前にリセット | 最後の受信メッセージの作成時刻より1秒前に設定 | 受信メッセージがある場合 |
| BR-40-2 | 担当者も更新 | 常にassignee_last_seen_atも更新（第2引数=true） | 常時 |
| BR-40-3 | バリデーションスキップ | update_columnsでバリデーション・コールバックをスキップ | 常時 |
| BR-40-4 | 受信メッセージなし | 受信メッセージがない場合はlast_seen_atがnilになる | 受信メッセージがない場合 |

### 計算ロジック

- 未読にする時刻: `last_incoming_message.created_at - 1.second`

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| unread | conversations | UPDATE | agent_last_seen_at, assignee_last_seen_at |
| unread | messages | SELECT | 最後の受信メッセージ取得 |

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

#### conversations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | agent_last_seen_at | last_incoming_message.created_at - 1秒 | |
| UPDATE | assignee_last_seen_at | last_incoming_message.created_at - 1秒 | |

#### messages

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | created_at | message_type = 'incoming', ORDER BY id DESC, LIMIT 1 | 最後の受信メッセージ |

## エラー処理

### エラーケース一覧

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

### リトライ仕様

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

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

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

## パフォーマンス要件

- update_columnsによりバリデーション・コールバックをスキップし、高速化
- 単純なDB更新のため、特別なパフォーマンス要件なし

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

- ConversationPolicy#show?による会話アクセス権限チェック

## 備考

- 「未読」機能は、実際には最終確認時刻を過去に戻すことで実現される
- unread_messagesは `messages.created_since(agent_last_seen_at)` で計算される
- update_columnsを使用しているため、updated_atコールバックは発動しない
- 受信メッセージが全くない会話では、agent_last_seen_atがnilになる

---

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

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

### 推奨読解順序

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

まず、未読メッセージの計算に関連するカラムとメソッドを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | conversation.rb | `app/models/conversation.rb` | agent_last_seen_at, unread_messagesメソッド |

**読解のコツ**: unread_messagesはagent_last_seen_at以降に作成されたメッセージを返す。last_seen_atを過去に戻すことで、それ以降のメッセージが「未読」として扱われる。

**主要処理フロー**:
- **7行目**: agent_last_seen_at カラム（スキーマコメント）
- **166-168行目**: unread_messages - agent_last_seen_at以降のメッセージ
- **145-147行目**: last_incoming_message - 最後の受信メッセージ

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

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

**主要処理フロー**:
- **125-129行目**: unread
  - messages.incoming.lastで最後の受信メッセージ取得
  - created_at - 1.secondで未読にする時刻を計算
  - update_last_seen_on_conversation(last_seen_at, true)で更新
- **153-160行目**: update_last_seen_on_conversation（再利用）
  - 第2引数=trueの場合、assignee_last_seen_atも更新

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

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

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

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

```
Api::V1::Accounts::ConversationsController#unread
    │
    ├─ conversation (before_action)
    │      └─ Current.account.conversations.find_by!
    │      └─ authorize @conversation, :show?
    │
    ├─ @conversation.messages.incoming.last
    │      └─ Message (最後の受信メッセージ)
    │
    ├─ last_seen_at計算
    │      └─ last_incoming_message.created_at - 1.second
    │
    └─ update_last_seen_on_conversation(last_seen_at, true)
           │
           └─ update_columns
                  ├─ agent_last_seen_at = last_seen_at
                  └─ assignee_last_seen_at = last_seen_at
```

### データフロー図

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

conversation_id ───▶ ConversationsController  ───▶ JSON Response
                            │                          (conversation)
                            ▼
                    messages.incoming.last
                            │
                            ▼
                    last_seen_at計算
                    (created_at - 1秒)
                            │
                            ▼
                update_last_seen_on_conversation
                            │
                            ▼
                    update_columns
                            │
                            ▼
                    conversations
                    (agent_last_seen_at,
                     assignee_last_seen_at)
```

### 関連ファイル一覧

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