# 機能設計書 45-メッセージ再送

## 概要

本ドキュメントは、Chatwootにおける送信失敗メッセージのリトライ機能の設計を記述します。

### 本機能の処理概要

送信に失敗したメッセージを再送信する機能を提供します。メッセージのステータスを「sent」に戻し、content_attributesをリセットした上で、SendReplyJobを再実行して各チャネルへの配信を試みます。

**業務上の目的・背景**：外部チャネルAPI（WhatsApp、Twitter、Telegram等）への送信は、ネットワーク障害やAPI制限などの理由で失敗することがあります。メッセージ再送機能により、エージェントは失敗したメッセージを手動で再送信でき、顧客への返信漏れを防止できます。また、一時的な障害が解消された後の再送信が容易になります。

**機能の利用シーン**：
- メッセージ送信が「failed」ステータスになった場合
- ネットワーク障害が復旧した後の再送信
- 外部APIのレート制限解除後の再送信

**主要な処理内容**：
1. メッセージステータスの「sent」への更新
2. content_attributesのリセット（external_error等のクリア）
3. SendReplyJobの再エンキュー
4. 各チャネルへの再配信

**関連システム・外部連携**：
- 各チャネルAPI（Facebook、WhatsApp、Telegram、Twitter等）
- SendReplyJob

**権限による制御**：会話にアクセス可能なエージェントがメッセージ再送を実行できます。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 11 | 会話詳細画面 | 主画面 | 失敗メッセージの再送ボタン |

## 機能種別

データ更新 / データ連携

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | Integer | Yes | 再送対象のメッセージID | 存在するメッセージID |

### 入力データソース

- API：`POST /api/v1/accounts/{account_id}/conversations/{conversation_id}/messages/{id}/retry`
- 画面入力：会話詳細画面の再送ボタン

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| message | Object | 更新されたメッセージオブジェクト |

### 出力先

- API レスポンス：JSON形式で更新されたメッセージ情報を返却
- データベース：messagesテーブルのstatus, content_attributesの更新
- ジョブキュー：SendReplyJobのエンキュー

## 処理フロー

### 処理シーケンス

```
1. APIリクエスト受信
   └─ retryアクションが呼び出される
2. メッセージ存在確認
   └─ message.blank?でnilチェック
3. ステータス更新
   └─ StatusUpdateServiceでstatusを'sent'に更新
4. content_attributes リセット
   └─ message.update!(content_attributes: {})
5. 再送信ジョブエンキュー
   └─ SendReplyJob.perform_later(message.id)
6. レスポンス返却
   └─ 更新されたメッセージをJSON形式で返却
```

### フローチャート

```mermaid
flowchart TD
    A[リトライリクエスト受信] --> B{メッセージ存在?}
    B -->|No| C[処理終了]
    B -->|Yes| D[StatusUpdateService実行]
    D --> E[status = 'sent']
    E --> F[content_attributes = {}]
    F --> G[SendReplyJob.perform_later]
    G --> H[チャネル配信]
    H --> I[メッセージ返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-45-001 | ステータスリセット | 再送時はステータスを'sent'にリセット | リトライ実行時 |
| BR-45-002 | エラー情報クリア | content_attributesを空にしてexternal_error等をクリア | リトライ実行時 |
| BR-45-003 | 即時ジョブ実行 | SendReplyJobは遅延なしで実行 | リトライ実行時 |

### 計算ロジック

特に複雑な計算ロジックはありません。

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ステータス更新 | messages | UPDATE | statusを'sent'に更新 |
| 属性リセット | messages | UPDATE | content_attributesを空に更新 |

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

#### messages

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | status | 0 (sent) | StatusUpdateService経由 |
| UPDATE | content_attributes | {} | external_error等をクリア |
| UPDATE | updated_at | 現在時刻 | 自動更新 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 | メッセージ未検出 | 指定されたIDのメッセージが存在しない | 正しいメッセージIDを指定 |
| 422 | 処理エラー | StatusUpdateServiceまたはupdateで例外発生 | エラーメッセージを確認して対処 |

### リトライ仕様

本機能自体がリトライ機能です。SendReplyJobが失敗した場合はSidekiqのデフォルトリトライが適用されます。

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

StatusUpdateService#performとmessage.update!は別々のトランザクションで実行されます。一方が失敗しても他方には影響しません。

## パフォーマンス要件

- リトライ処理は同期（メッセージ更新）と非同期（ジョブエンキュー）の組み合わせ
- SendReplyJobはhighキューで優先実行

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

- 会話へのアクセス権限チェック（BaseControllerで実施）
- アカウント境界を超えたメッセージへのアクセスは不可

## 備考

- 再送は手動トリガーのみで、自動リトライは行いません
- 再送後も失敗する場合は、根本原因（APIキー期限切れ、アカウント停止等）を確認する必要があります

---

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

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

### 推奨読解順序

#### 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です。リトライ時は'sent'(0)に戻します。

#### Step 2: エントリーポイントを理解する

APIエンドポイントの処理を確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | messages_controller.rb | `app/controllers/api/v1/accounts/conversations/messages_controller.rb` | retryアクション（28-37行目） |

**主要処理フロー**:
1. **29行目**: message.blank?でnilチェック
2. **31-32行目**: StatusUpdateServiceでステータスを'sent'に更新
3. **33行目**: content_attributesを空にリセット
4. **34行目**: SendReplyJobをエンキュー

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

ステータス更新のバリデーションを確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | status_update_service.rb | `app/services/messages/status_update_service.rb` | performとvalid_status_transition（10-33行目） |

**主要処理フロー**:
- **10-14行目**: valid_status_transition?でステータス遷移の妥当性チェック
- **18-23行目**: update_message_statusでステータスとexternal_errorを更新

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

```
MessagesController#retry
    │
    ├─ message.blank? チェック
    │
    ├─ Messages::StatusUpdateService#perform
    │      │
    │      ├─ valid_status_transition?
    │      │      └─ Message.statuses.key?(status)
    │      │
    │      └─ update_message_status
    │             └─ message.update!(status: 'sent', external_error: nil)
    │
    ├─ message.update!(content_attributes: {})
    │
    └─ SendReplyJob.perform_later(message.id)
           │
           └─ Channel Service (Twitter/Twilio/Line等)
```

### データフロー図

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

message_id          ───▶ MessagesController#retry      ───▶ DB (messages.status = 'sent')
                              │                               │
                              │                               ▼
                              │                         DB (messages.content_attributes = {})
                              │
                              └─▶ SendReplyJob          ───▶ 各チャネルAPI
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| messages_controller.rb | `app/controllers/api/v1/accounts/conversations/messages_controller.rb` | コントローラー | リトライAPIエンドポイント |
| status_update_service.rb | `app/services/messages/status_update_service.rb` | サービス | ステータス更新ロジック |
| send_reply_job.rb | `app/jobs/send_reply_job.rb` | ジョブ | チャネル配信ジョブ |
| message.rb | `app/models/message.rb` | モデル | メッセージデータモデル |
