# 機能設計書 50-メッセージステータス追跡

## 概要

本ドキュメントは、Chatwootにおけるメッセージの送信・配信・既読状態の追跡機能の設計を記述します。

### 本機能の処理概要

メッセージのライフサイクルを追跡し、送信（sent）、配信完了（delivered）、既読（read）、失敗（failed）の各状態を管理する機能を提供します。外部チャネルからのWebhook通知やAPIインボックスからのステータス更新を処理し、UIにリアルタイムでステータスを反映します。

**業務上の目的・背景**：エージェントは送信したメッセージが顧客に確実に届いたか、顧客が読んだかを把握する必要があります。メッセージステータス追跡機能により、コミュニケーションの進捗状況を可視化し、未配信や失敗したメッセージへの対応を促進します。特にWhatsAppやSMS等の外部チャネルでは、配信・既読の確認が重要です。

**機能の利用シーン**：
- エージェントがメッセージの配信状況を確認する場面
- 送信失敗時にリトライを検討する場面
- 外部チャネルからの配信・既読レポートを受信する場面
- APIインボックスで外部システムと状態を同期する場面

**主要な処理内容**：
1. メッセージ作成時の初期ステータス（sent）設定
2. 外部チャネルからの配信・既読レポート処理
3. StatusUpdateServiceによるステータス遷移管理
4. ステータス変更時のMESSAGE_UPDATEDイベント発火
5. 失敗時のexternal_error記録

**関連システム・外部連携**：
- 各チャネルのWebhook（WhatsApp、Facebook、Telegram等）
- WebSocket（ActionCable）：ステータス変更の通知
- APIインボックス：外部システムからのステータス更新

**権限による制御**：ステータス更新APIはAPIインボックスのみ利用可能です。外部チャネルからのWebhookは認証トークンで検証されます。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 11 | 会話詳細画面 | 主画面 | メッセージステータスの表示 |

## 機能種別

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

## 入力仕様

### 入力パラメータ（ステータス更新API）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | Integer | Yes | 更新対象のメッセージID | 存在するメッセージID |
| status | String | Yes | 新しいステータス | sent/delivered/read/failed |
| external_error | String | No | 外部エラーメッセージ | status=failedの場合のみ有効 |

### 入力データソース

- API（更新）：`PATCH /api/v1/accounts/{account_id}/conversations/{conversation_id}/messages/{id}`
- 外部Webhook：各チャネルからの配信・既読レポート

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| status | Integer | メッセージステータス（0:sent, 1:delivered, 2:read, 3:failed） |
| external_error | String | 外部エラーメッセージ（failed時のみ） |

### 出力先

- API レスポンス：JSON形式で更新されたメッセージ情報を返却
- データベース：messagesテーブルのstatus, external_error
- WebSocket：MESSAGE_UPDATEDイベント

## 処理フロー

### 処理シーケンス

```
1. メッセージ作成時
   └─ status = sent（デフォルト）
2. チャネル配信後
   └─ 成功時はstatus = sent維持、失敗時はstatus = failed
3. 配信確認受信時
   └─ status = delivered
4. 既読確認受信時
   └─ status = read
5. ステータス更新時
   └─ StatusUpdateServiceでバリデーション後更新
6. 更新後
   └─ after_update_commitでMESSAGE_UPDATEDイベント発火
```

### フローチャート

```mermaid
flowchart TD
    A[メッセージ作成] --> B[status = sent]
    B --> C{チャネル配信}
    C -->|成功| D[配信処理継続]
    C -->|失敗| E[status = failed]
    E --> F[external_error設定]
    D --> G{配信レポート受信}
    G -->|delivered| H[status = delivered]
    G -->|なし| I[status = sent維持]
    H --> J{既読レポート受信}
    I --> J
    J -->|read| K[status = read]
    J -->|なし| L[現状維持]

    subgraph StatusUpdateService
    M[ステータス更新リクエスト] --> N{valid_status_transition?}
    N -->|Yes| O[update_message_status]
    N -->|No| P[false返却]
    O --> Q[MESSAGE_UPDATEDイベント]
    end
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-50-001 | 初期ステータス | メッセージ作成時のデフォルトステータスはsent | メッセージ作成時 |
| BR-50-002 | ステータス遷移検証 | Message.statuses.key?(status)でステータス値を検証 | ステータス更新時 |
| BR-50-003 | read→delivered禁止 | read状態からdeliveredへの変更は不可 | ステータス更新時 |
| BR-50-004 | external_error条件 | external_errorはstatus=failedの場合のみ設定 | ステータス更新時 |
| BR-50-005 | APIインボックス限定 | ステータス更新APIはAPIインボックスのみ利用可能 | API更新時 |

### 計算ロジック

**ステータス遷移検証**:
```ruby
def valid_status_transition?
  return false unless Message.statuses.key?(status)
  return false if message.read? && status == 'delivered'
  true
end
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ステータス更新 | messages | UPDATE | status, external_errorの更新 |

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

#### messages

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | status | sent(0)/delivered(1)/read(2)/failed(3) | enum |
| UPDATE | external_error | エラーメッセージ | content_attributes内、failed時のみ |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 403 | 権限エラー | APIインボックス以外でステータス更新を試行 | APIインボックス経由で操作 |
| 422 | バリデーションエラー | 無効なステータス値 | 有効なステータス値を指定 |
| - | 無視 | read→deliveredの遷移試行 | falseが返却されるが例外は発生しない |

### リトライ仕様

特にリトライ処理は実装されていません。ステータス更新は冪等性を持ちます。

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

単一レコードの更新のため、明示的なトランザクション制御は不要です。

## パフォーマンス要件

- ステータス更新は同期処理
- after_update_commitでのイベント発火は非同期

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

- APIインボックスチェック（ensure_api_inbox before_action）
- 外部WebhookはチャネルごとのWebhook署名検証
- アカウント境界を超えたメッセージへのアクセスは不可

## 備考

- ステータス変更はUIにリアルタイムで反映されます（WebSocket経由）
- 各チャネルのWebhookハンドラーでステータス更新が行われます
- failedステータスのメッセージはリトライ機能で再送信可能です

---

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

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

### 推奨読解順序

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

まず、メッセージのステータス定義を確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | message.rb | `app/models/message.rb` | status enum定義（101行目）、content_attributes store（109-111行目） |

**読解のコツ**: statusは0:sent, 1:delivered, 2:read, 3:failedのenum定義です。external_errorはcontent_attributesのstoreアクセサです。

#### Step 2: ステータス更新サービスを理解する

StatusUpdateServiceの処理を確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | status_update_service.rb | `app/services/messages/status_update_service.rb` | 全体（1-35行目） |

**主要処理フロー**:
1. **3-8行目**: 初期化でmessage, status, external_errorを受け取る
2. **10-14行目**: performでvalid_status_transition?チェック後更新
3. **26-33行目**: valid_status_transition?でステータス遷移を検証
4. **18-23行目**: update_message_statusでstatusとexternal_errorを更新

#### Step 3: APIエンドポイントを理解する

コントローラーでの処理を確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | messages_controller.rb | `app/controllers/api/v1/accounts/conversations/messages_controller.rb` | updateアクション（16-19行目）、ensure_api_inbox（75-79行目） |

**主要処理フロー**:
- **2行目**: before_action :ensure_api_inboxでAPIインボックスチェック
- **17行目**: StatusUpdateServiceを呼び出し

#### Step 4: イベント通知を理解する

ステータス変更後のイベント発火を確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | message.rb | `app/models/message.rb` | after_update_commit :dispatch_update_event（137行目）、send_update_event（232-234行目） |

**主要処理フロー**:
- **137行目**: after_update_commitでdispatch_update_eventが呼ばれる
- **232-234行目**: send_update_eventでMESSAGE_UPDATEDイベントを発火

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

```
MessagesController#update
    │
    ├─ ensure_api_inbox (before_action)
    │      └─ 403 if not API inbox
    │
    └─ Messages::StatusUpdateService#perform
           │
           ├─ valid_status_transition?
           │      ├─ Message.statuses.key?(status)
           │      └─ !(message.read? && status == 'delivered')
           │
           └─ update_message_status
                  │
                  └─ message.update!(status: status, external_error: ...)
                         │
                         └─ after_update_commit :dispatch_update_event
                                │
                                └─ send_update_event
                                       │
                                       └─ dispatcher.dispatch(MESSAGE_UPDATED)
```

### データフロー図

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

status              ───▶ StatusUpdateService          ───▶ DB (messages.status)
external_error            │
                          │
                          ├─▶ valid_status_transition?
                          │      │
                          │      └─▶ false (遷移不可)
                          │
                          └─▶ update_message_status   ───▶ DB (messages.external_error)
                                 │
                                 └─▶ after_update_commit ───▶ WebSocket
                                                              (MESSAGE_UPDATED)

外部Webhook         ───▶ チャネルサービス             ───▶ StatusUpdateService
(配信/既読レポート)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| status_update_service.rb | `app/services/messages/status_update_service.rb` | サービス | ステータス更新ロジック |
| messages_controller.rb | `app/controllers/api/v1/accounts/conversations/messages_controller.rb` | コントローラー | ステータス更新API |
| message.rb | `app/models/message.rb` | モデル | ステータス定義、イベント発火 |
