# 機能設計書 46-メッセージ削除・編集

## 概要

本ドキュメントは、Chatwootにおけるメッセージの削除・編集機能の設計を記述します。

### 本機能の処理概要

送信済みメッセージの削除および更新（ステータス変更）機能を提供します。メッセージ削除は論理削除として実装されており、メッセージ内容を「このメッセージは削除されました」に置き換え、添付ファイルを物理削除します。メッセージ更新はAPIインボックス専用で、外部システムからのステータス同期に使用されます。

**業務上の目的・背景**：エージェントが誤って送信したメッセージや不適切な内容のメッセージを削除する必要があります。また、APIインボックスでは外部システムとのメッセージステータス同期が必要です。論理削除により、会話の流れを維持しつつ、問題のあるコンテンツを安全に除去できます。

**機能の利用シーン**：
- エージェントが誤送信したメッセージを削除する場面
- 不適切な内容を含むメッセージを削除する場面
- APIインボックスで外部システムからのステータス更新を受信する場面

**主要な処理内容**：
1. メッセージの論理削除（内容置換）
2. 添付ファイルの物理削除
3. メッセージステータスの更新（APIインボックスのみ）
4. MESSAGE_UPDATEDイベントの発火

**関連システム・外部連携**：
- WebSocket（ActionCable）：更新通知
- Active Storage：添付ファイル削除

**権限による制御**：会話にアクセス可能なエージェントがメッセージを削除できます。ステータス更新はAPIインボックスのみ許可されます。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 11 | 会話詳細画面 | 主画面 | メッセージの編集・削除 |

## 機能種別

CRUD操作 / データ更新

## 入力仕様

### 入力パラメータ（削除）

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

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

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

### 入力データソース

- API（削除）：`DELETE /api/v1/accounts/{account_id}/conversations/{conversation_id}/messages/{id}`
- API（更新）：`PATCH /api/v1/accounts/{account_id}/conversations/{conversation_id}/messages/{id}`
- 画面入力：会話詳細画面のメッセージ削除ボタン

## 出力仕様

### 出力データ（削除）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | Integer | 削除されたメッセージID |
| content | String | 「このメッセージは削除されました」（i18n） |
| content_type | String | text |
| content_attributes | Hash | {deleted: true} |

### 出力データ（更新）

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

### 出力先

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

## 処理フロー

### 処理シーケンス（削除）

```
1. APIリクエスト受信
   └─ destroyアクションが呼び出される
2. トランザクション開始
   └─ ActiveRecord::Base.transaction
3. メッセージ更新
   └─ content, content_type, content_attributesを更新
4. 添付ファイル削除
   └─ message.attachments.destroy_all
5. トランザクションコミット
   └─ 成功時のみコミット
6. After Update Commit
   └─ dispatch_update_eventが呼び出される
7. レスポンス返却
   └─ 削除されたメッセージをJSON形式で返却
```

### 処理シーケンス（更新）

```
1. APIリクエスト受信
   └─ updateアクションが呼び出される
2. APIインボックスチェック
   └─ ensure_api_inboxでAPIインボックス以外は403
3. ステータス更新
   └─ StatusUpdateServiceで更新
4. レスポンス返却
   └─ 更新されたメッセージをJSON形式で返却
```

### フローチャート

```mermaid
flowchart TD
    subgraph 削除処理
    A1[削除リクエスト受信] --> B1[トランザクション開始]
    B1 --> C1[content更新]
    C1 --> D1[content_type = text]
    D1 --> E1[content_attributes = deleted:true]
    E1 --> F1[attachments.destroy_all]
    F1 --> G1[トランザクションコミット]
    G1 --> H1[MESSAGE_UPDATEDイベント]
    end

    subgraph 更新処理
    A2[更新リクエスト受信] --> B2{APIインボックス?}
    B2 -->|No| C2[403 Forbidden]
    B2 -->|Yes| D2[StatusUpdateService実行]
    D2 --> E2[メッセージ返却]
    end
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-46-001 | 論理削除 | メッセージは物理削除されず、内容が置換される | 削除時 |
| BR-46-002 | 削除メッセージ内容 | content は i18n('conversations.messages.deleted')に置換 | 削除時 |
| BR-46-003 | 添付ファイル削除 | 添付ファイルは物理削除される | 削除時 |
| BR-46-004 | 削除フラグ | content_attributes.deletedがtrueに設定される | 削除時 |
| BR-46-005 | APIインボックス限定 | ステータス更新はAPIインボックスのみ許可 | 更新時 |
| BR-46-006 | read→delivered禁止 | read状態からdeliveredへの変更は不可 | ステータス更新時 |

### 計算ロジック

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

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| メッセージ削除 | messages | UPDATE | content, content_type, content_attributesの更新 |
| 添付ファイル削除 | attachments | DELETE | 関連する全添付ファイルの削除 |
| ステータス更新 | messages | UPDATE | status, external_errorの更新 |

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

#### messages（削除時）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | content | I18n.t('conversations.messages.deleted') | i18n対応 |
| UPDATE | content_type | text | enum値0 |
| UPDATE | content_attributes | {deleted: true} | 削除フラグ |

#### attachments（削除時）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| DELETE | - | message_id = 対象メッセージID | 物理削除 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 403 | 権限エラー | APIインボックス以外でステータス更新を試行 | APIインボックス経由で操作 |
| 404 | メッセージ未検出 | 指定されたIDのメッセージが存在しない | 正しいメッセージIDを指定 |
| 422 | バリデーションエラー | 無効なステータス値 | 有効なステータス値を指定 |

### リトライ仕様

特にリトライ処理は実装されていません。

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

削除処理はActiveRecord::Base.transactionで以下を一括処理：
1. メッセージのcontent, content_type, content_attributes更新
2. 添付ファイルの削除

いずれかが失敗した場合は全体がロールバックされます。

## パフォーマンス要件

- 削除処理は同期で実行
- 添付ファイルが多い場合はdestroy_allの実行時間に注意

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

- 会話へのアクセス権限チェック（BaseControllerで実施）
- APIインボックスチェック（ensure_api_inbox before_action）
- 削除されたメッセージの元内容は復元不可

## 備考

- 削除されたメッセージはUIで「削除済み」として表示されます
- 外部チャネルへの削除同期は行われません（チャネル側にメッセージは残ります）
- ステータス更新は主に外部APIからのWebhook処理で使用されます

---

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

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

### 推奨読解順序

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

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

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

**読解のコツ**: content_attributesにはdeletedキーが含まれ、UIはこのフラグを見て削除済み表示を行います。

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

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

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

**主要処理フロー（削除）**:
1. **22行目**: トランザクション開始
2. **23行目**: content, content_type, content_attributesを更新
3. **24行目**: attachments.destroy_allで添付ファイル削除

**主要処理フロー（更新）**:
1. **2行目**: before_action :ensure_api_inboxでAPIインボックスチェック
2. **17行目**: StatusUpdateServiceでステータス更新

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

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

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

**主要処理フロー**:
- **27行目**: Message.statuses.key?(status)で有効なステータス値かチェック
- **30行目**: read状態からdeliveredへの遷移を禁止

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

更新後のイベント発火を確認します。

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

**主要処理フロー**:
- **137行目**: after_update_commit :dispatch_update_event
- **370-372行目**: previous_changes.blank?なら何もしない
- **374行目**: send_update_eventでMESSAGE_UPDATEDを発火

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

```
MessagesController#destroy
    │
    └─ ActiveRecord::Base.transaction
           │
           ├─ message.update!
           │      ├─ content = I18n.t('conversations.messages.deleted')
           │      ├─ content_type = :text
           │      └─ content_attributes = {deleted: true}
           │
           └─ message.attachments.destroy_all
                  │
                  └─ Active Storage cleanup

    after_update_commit
           │
           └─ dispatch_update_event
                  │
                  └─ send_update_event
                         │
                         └─ dispatcher.dispatch(MESSAGE_UPDATED)

MessagesController#update
    │
    ├─ ensure_api_inbox (before_action)
    │      └─ 403 if not API inbox
    │
    └─ Messages::StatusUpdateService#perform
           │
           ├─ valid_status_transition?
           │
           └─ update_message_status
```

### データフロー図

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

message_id          ───▶ MessagesController#destroy    ───▶ DB (messages更新)
                              │                               │
                              │                               ▼
                              │                         DB (attachments削除)
                              │
                              └─▶ dispatch_update_event ───▶ WebSocket
                                                              (MESSAGE_UPDATED)

message_id          ───▶ MessagesController#update     ───▶ DB (messages.status更新)
status                    │
external_error            └─▶ StatusUpdateService
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| messages_controller.rb | `app/controllers/api/v1/accounts/conversations/messages_controller.rb` | コントローラー | 削除・更新APIエンドポイント |
| status_update_service.rb | `app/services/messages/status_update_service.rb` | サービス | ステータス更新ロジック |
| message.rb | `app/models/message.rb` | モデル | メッセージデータモデル、イベント発火 |
| attachment.rb | `app/models/attachment.rb` | モデル | 添付ファイルモデル |
