# 機能設計書 58-連絡先マージ

## 概要

本ドキュメントは、Chatwootにおける連絡先マージ機能について記述する。本機能は、重複して作成された複数の連絡先レコードを1つに統合し、関連するデータ（会話、メッセージ、ノート等）を引き継ぐ機能である。

### 本機能の処理概要

**業務上の目的・背景**：同一顧客が複数のチャネルや異なるタイミングで問い合わせを行うと、重複した連絡先が作成される場合がある。これらを手動で統合することで、顧客情報を一元化し、過去の対応履歴を参照しやすくする。また、顧客がメールアドレスを変更した場合など、情報の更新に伴う重複を解消できる。

**機能の利用シーン**：
- 同一顧客が異なるチャネルから問い合わせて複数の連絡先が作成された場合
- 顧客がメールアドレスや電話番号を変更した場合
- データインポート時に重複が発生した場合
- 顧客情報の整理・クリーンアップ時

**主要な処理内容**：
1. ベース連絡先とマージ対象連絡先の指定
2. 会話の所有者変更（マージ対象 -> ベース）
3. メッセージの送信者変更
4. 連絡先インボックスの移行
5. ノートの移行
6. 属性のマージ（ベースを優先）
7. マージ対象連絡先の削除
8. CONTACT_MERGEDイベントの発行

**関連システム・外部連携**：WebSocketによるリアルタイム通知（pubsub_token更新）。

**権限による制御**：連絡先マージはエージェント権限以上が必要。両方の連絡先が同一アカウントに属することが必須。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 24 | 連絡先編集画面 | 主画面 | 連絡先マージの実行 |

## 機能種別

データ統合 / バルク更新

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| base_contact_id | Integer | Yes | ベース連絡先ID（統合先） | 存在する連絡先 |
| mergee_contact_id | Integer | Yes | マージ対象連絡先ID（統合元） | 存在する連絡先 |

### 入力データソース

- 管理画面からの連絡先マージリクエスト
- APIリクエスト

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| base_contact | Contact | マージ後のベース連絡先 |

### 出力先

- contacts テーブル（更新）
- conversations テーブル（contact_id更新）
- messages テーブル（sender_id更新）
- contact_inboxes テーブル（contact_id更新）
- notes テーブル（contact_id更新）
- WebSocket通知

## 処理フロー

### 処理シーケンス

```
1. マージリクエスト受信
   └─ base_contact_id, mergee_contact_idを取得
2. 連絡先取得・バリデーション
   └─ 両方の連絡先が同一アカウントに属することを確認
3. 同一連絡先チェック
   └─ 同じIDの場合は即座に返却
4. トランザクション開始
5. 会話の移行
   └─ mergee_contactの会話をbase_contactに変更
6. メッセージの移行
   └─ mergee_contactが送信者のメッセージをbase_contactに変更
7. 連絡先インボックスの移行
   └─ mergee_contactのcontact_inboxをbase_contactに変更
8. ノートの移行
   └─ mergee_contactのノートをbase_contactに変更
9. 属性のマージ
   └─ 属性をマージし、mergee_contactを削除
   └─ base_contactを更新
10. イベント発行
    └─ CONTACT_MERGEDイベントをディスパッチ
11. トランザクションコミット
```

### フローチャート

```mermaid
flowchart TD
    A[マージリクエスト] --> B[連絡先取得]
    B --> C{同一アカウント?}
    C -->|No| Z1[エラー: アカウント不一致]
    C -->|Yes| D{同一ID?}
    D -->|Yes| Z2[ベース連絡先を返却]
    D -->|No| E[トランザクション開始]
    E --> F[会話の移行]
    F --> G[メッセージの移行]
    G --> H[連絡先インボックスの移行]
    H --> I[ノートの移行]
    I --> J[属性のマージ]
    J --> K[mergee_contact削除]
    K --> L[base_contact更新]
    L --> M[CONTACT_MERGEDイベント発行]
    M --> N[トランザクションコミット]
    N --> O[ベース連絡先を返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-58-01 | 同一アカウント必須 | 両方の連絡先が同一アカウントに属すること | 常時 |
| BR-58-02 | ベース優先 | 属性マージ時はベース連絡先の値を優先 | 属性マージ時 |
| BR-58-03 | 同一ID許容 | 同一連絡先を指定した場合はエラーにせず即座に返却 | 常時 |
| BR-58-04 | 削除順序 | mergee_contactをreloadしてから削除 | トランザクション内 |

### マージ対象属性

| 属性名 | マージ方法 |
|--------|-----------|
| identifier | ベース優先（空でなければ） |
| name | ベース優先（空でなければ） |
| email | ベース優先（空でなければ） |
| phone_number | ベース優先（空でなければ） |
| additional_attributes | ディープマージ（ベース優先） |
| custom_attributes | ディープマージ（ベース優先） |

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 会話移行 | conversations | UPDATE | contact_idを変更 |
| メッセージ移行 | messages | UPDATE | senderを変更 |
| 連絡先インボックス移行 | contact_inboxes | UPDATE | contact_idを変更 |
| ノート移行 | notes | UPDATE | contact_idを変更 |
| マージ対象削除 | contacts | DELETE | mergee_contactを削除 |
| ベース更新 | contacts | UPDATE | マージ後の属性で更新 |

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

#### conversations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | contact_id | base_contact.id | mergee_contact_idの会話を更新 |

#### messages

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | sender_id | base_contact.id | mergee_contactが送信者のメッセージ |
| UPDATE | sender_type | 'Contact' | 変更なし |

#### contact_inboxes

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | contact_id | base_contact.id | mergee_contact_idのレコード |

#### notes

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | contact_id | base_contact.id | mergee_contact_idのノート |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | StandardError | 連絡先がアカウントに属さない | エラーメッセージを返却 |
| 404 | 存在しないリソース | 指定IDの連絡先が存在しない | 404 Not Foundを返却 |

### リトライ仕様

なし（トランザクション内で同期処理）

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

全てのデータ移行・削除処理はActiveRecord::Base.transactionでラップされ、原子性を保証。いずれかの処理が失敗した場合は全体がロールバックされる。

## パフォーマンス要件

- 各テーブルの更新は一括UPDATE
- トランザクション内での処理は通常500ms以内で完了

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

- 同一アカウント内の連絡先のみマージ可能
- マージ権限を持つユーザーのみが実行可能
- マージ後はpubsub_tokenが更新され、WebSocket接続が更新される

## 備考

- マージ後はmergee_contactは完全に削除される
- contact_inboxesのpubsub_tokenは維持され、CONTACT_MERGEDイベントで通知

---

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

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

### 推奨読解順序

#### Step 1: コントローラーのマージアクションを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | contact_merges_controller.rb | `app/controllers/api/v1/accounts/actions/contact_merges_controller.rb` | マージAPIエンドポイント |

**主要処理フロー**:
- **5-12行目**: createアクションでContactMergeActionを呼び出し
- **16-17行目**: set_base_contactでベース連絡先を取得
- **20-21行目**: set_mergee_contactでマージ対象を取得

#### Step 2: マージアクションを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | contact_merge_action.rb | `app/actions/contact_merge_action.rb` | マージロジックの実装 |

**主要処理フロー**:
- **5-18行目**: performメソッドでトランザクション内マージ
- **8行目**: 同一IDチェック
- **33-34行目**: merge_conversationsで会話を移行
- **37-38行目**: merge_contact_notesでノートを移行
- **41-42行目**: merge_messagesでメッセージを移行
- **45-46行目**: merge_contact_inboxesで連絡先インボックスを移行
- **49-61行目**: merge_and_remove_mergee_contactで属性マージと削除

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

```
Api::V1::Accounts::Actions::ContactMergesController#create
    │
    └─ ContactMergeAction#perform
           │
           ├─ validate_contacts
           │
           ├─ merge_conversations
           │      └─ Conversation.where(contact_id: mergee).update(contact_id: base)
           │
           ├─ merge_messages
           │      └─ Message.where(sender: mergee).update(sender: base)
           │
           ├─ merge_contact_inboxes
           │      └─ ContactInbox.where(contact_id: mergee).update(contact_id: base)
           │
           ├─ merge_contact_notes
           │      └─ Note.where(contact_id: mergee).update(contact_id: base)
           │
           └─ merge_and_remove_mergee_contact
                  ├─ 属性マージ（deep_merge）
                  ├─ mergee_contact.destroy!
                  ├─ CONTACT_MERGED イベント発行
                  └─ base_contact.update!
```

### データフロー図

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

base_contact_id    ContactMergeAction        base_contact (更新済み)
mergee_contact_id ───▶ トランザクション内   ───▶  │
                       各テーブル更新              ▼
                       mergee削除            CONTACT_MERGEDイベント
                       base更新                   │
                                                  ▼
                                            WebSocket通知
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| contact_merges_controller.rb | `app/controllers/api/v1/accounts/actions/contact_merges_controller.rb` | コントローラー | マージAPIエンドポイント |
| contact_merge_action.rb | `app/actions/contact_merge_action.rb` | アクション | マージロジック実装 |
