# 機能設計書 9-取引先へのアタッチ/ディスカード

## 概要

本ドキュメントは、Fat Free CRMシステムにおける取引先へのアタッチ/ディスカード機能の設計を定義する。この機能は、取引先に関連エンティティ（連絡先、商談、タスク）を紐付け/解除する。

### 本機能の処理概要

取引先へのアタッチ/ディスカード機能は、取引先と他のエンティティ（連絡先、商談、タスク）との関連付けを管理する機能である。既存のエンティティを取引先に紐付けたり、紐付けを解除したりできる。

**業務上の目的・背景**：CRMでは、取引先（企業）と連絡先（担当者）、商談（ビジネス機会）、タスク（やるべきこと）が相互に関連している。これらの関連を動的に管理することで、実際のビジネス関係の変化に対応できる。例えば、連絡先が別の企業に転職した場合や、商談が別の取引先に移管された場合に、紐付けを変更する必要がある。

**機能の利用シーン**：
- 既存の連絡先を取引先に紐付ける場面
- 商談を取引先に関連付ける場面
- タスクを取引先に割り当てる場面
- 誤って紐付けたエンティティを解除する場面
- 連絡先が転職して別取引先に移動する場面

**主要な処理内容**：
1. 紐付け対象エンティティの取得
2. 紐付け処理（attach）または解除処理（discard）の実行
3. 取引先データの再読み込み
4. AJAX応答の返却

**関連システム・外部連携**：本機能は外部システムとの連携はなく、内部データベースの更新のみで構成される。

**権限による制御**：`load_and_authorize_resource`により、ユーザーが編集権限を持つ取引先のみ操作可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 10 | 取引先編集フォーム | 補助機能 | 関連エンティティの紐付け/解除 |

## 機能種別

データ更新（UPDATE操作）/ 関連付け管理

## 入力仕様

### 入力パラメータ

#### attach（紐付け）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | Integer | Yes | 取引先ID | 存在する取引先IDであること |
| assets | String | Yes | 紐付け対象モデル名（contacts/opportunities/tasks） | 有効なモデル名 |
| asset_id | Integer | Yes | 紐付け対象ID | 存在するエンティティID |

#### discard（解除）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | Integer | Yes | 取引先ID | 存在する取引先IDであること |
| attachment | String | Yes | 解除対象モデル名（Contact/Opportunity/Task） | 有効なモデル名 |
| attachment_id | Integer | Yes | 解除対象ID | 存在するエンティティID |

### 入力データソース

- URLパラメータ
- セッション情報（current_user）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| @account | Account | 更新された取引先（再読み込み済み） |
| @attachment | Contact/Opportunity/Task | 紐付け/解除されたエンティティ |
| @attached | Boolean/Array | 紐付け結果（attach時） |

### 出力先

- AJAX応答（attach.js.haml、discard.js.haml）
- データベース（account_contacts、account_opportunities、tasks）

## 処理フロー

### 処理シーケンス

```
【attachアクション】
1. load_and_authorize_resourceによるリソースロード・認可
2. find_class(params[:assets])で紐付け対象クラス取得
3. 対象クラスからfind(params[:asset_id])でエンティティ取得
4. entity.attach!(@attachment)で紐付け実行
5. entity.reloadで再読み込み
6. respond_with(entity)

【discardアクション】
1. load_and_authorize_resourceによるリソースロード・認可
2. find_class(params[:attachment])で解除対象クラス取得
3. 対象クラスからfind(params[:attachment_id])でエンティティ取得
4. entity.discard!(@attachment)で解除実行
5. entity.reloadで再読み込み
6. respond_with(entity)
```

### フローチャート

```mermaid
flowchart TD
    A[attachリクエスト] --> B[認可チェック]
    B --> C{認可OK?}
    C -->|No| D[403エラー]
    C -->|Yes| E[対象クラス取得]
    E --> F[対象エンティティ取得]
    F --> G[attach!実行]
    G --> H[reload]
    H --> I[AJAX応答]

    J[discardリクエスト] --> K[認可チェック]
    K --> L{認可OK?}
    L -->|No| M[403エラー]
    L -->|Yes| N[対象クラス取得]
    N --> O[対象エンティティ取得]
    O --> P[discard!実行]
    P --> Q[reload]
    Q --> R[AJAX応答]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | 重複紐付け防止 | 既に紐付いているエンティティは再紐付けしない | attach時 |
| BR-002 | タスク解除 | タスクの場合はasset属性をnilに設定 | discardでTaskの場合 |
| BR-003 | 連絡先/商談解除 | 中間テーブルからレコードを削除 | discardでContact/Opportunityの場合 |

### 計算ロジック

**attach!メソッド（Accountモデル）**:
```ruby
def attach!(attachment)
  send(attachment.class.name.tableize) << attachment unless send("#{attachment.class.name.downcase}_ids").include?(attachment.id)
end
```

**discard!メソッド（Accountモデル）**:
```ruby
def discard!(attachment)
  if attachment.is_a?(Task)
    attachment.update_attribute(:asset, nil)
  else # Contacts, Opportunities
    send(attachment.class.name.tableize).delete(attachment)
  end
end
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 連絡先紐付け | account_contacts | INSERT | 紐付けレコード作成 |
| 商談紐付け | account_opportunities | INSERT | 紐付けレコード作成 |
| タスク紐付け | tasks | UPDATE | asset_type, asset_id設定 |
| 連絡先解除 | account_contacts | DELETE | 紐付けレコード削除 |
| 商談解除 | account_opportunities | DELETE | 紐付けレコード削除 |
| タスク解除 | tasks | UPDATE | asset_type, asset_idをnull |

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

#### account_contacts

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | account_id, contact_id | @account.id, @attachment.id | attach |
| DELETE | - | account_id, contact_id | discard |

#### account_opportunities

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | account_id, opportunity_id | @account.id, @attachment.id | attach |
| DELETE | - | account_id, opportunity_id | discard |

#### tasks

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | asset_type, asset_id | 'Account', @account.id | attach |
| UPDATE | asset_type, asset_id | nil, nil | discard |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 401 | 認証エラー | 未ログイン状態でアクセス | ログイン画面へリダイレクト |
| 403 | 認可エラー | 編集権限がない | エラーメッセージ表示 |
| 404 | 未検出 | 指定IDのエンティティが存在しない | エラーメッセージ表示 |

### リトライ仕様

本機能ではリトライ処理は実装されていない。

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

紐付け/解除処理は単一のデータベース操作であり、明示的なトランザクション制御は不要。

## パフォーマンス要件

- 処理時間: 1秒以内を目標

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

- 認証: Deviseによるユーザー認証が必須
- 認可: CanCanによるアクセス権限制御
- CSRF: protect_from_forgeryによる対策
- クラス名検証: find_classで有効なクラス名のみ許可

## 備考

- EntitiesControllerに共通実装されており、全エンティティコントローラーで利用可能
- UIはセレクトボックスとボタンで操作

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | account.rb | `app/models/entities/account.rb` | attach!, discard!メソッド |
| 1-2 | account_contact.rb | `app/models/entities/account_contact.rb` | 中間テーブル |
| 1-3 | account_opportunity.rb | `app/models/entities/account_opportunity.rb` | 中間テーブル |

**読解のコツ**: attach!（103-105行目）とdiscard!（109-115行目）の実装を確認。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | routes.rb | `config/routes.rb` | PUT /accounts/:id/attach, POST /accounts/:id/discard ルート |
| 2-2 | entities_controller.rb | `app/controllers/entities_controller.rb` | attach, discardアクション |

**主要処理フロー**:
1. **23-29行目**: attachアクション
2. **24行目**: find_classで対象クラス取得
3. **25行目**: entity.attach!で紐付け
4. **31-39行目**: discardアクション
5. **34行目**: find_classで対象クラス取得
6. **35行目**: entity.discard!で解除

#### Step 3: find_classメソッドを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | application_controller.rb | `app/controllers/application_controller.rb` | find_classメソッド |

**主要処理フロー**:
- **261-270行目**: find_class - 有効なクラス名の検証と取得

#### Step 4: ビュー層を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | attach.js.haml | `app/views/accounts/attach.js.haml` | 紐付け結果JS |
| 4-2 | discard.js.haml | `app/views/accounts/discard.js.haml` | 解除結果JS |

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

```
EntitiesController#attach
    │
    ├─ load_and_authorize_resource (Account.find)
    │
    ├─ find_class(params[:assets])
    │   └─ ActiveRecord::Base.descendants から検索
    │
    ├─ @attachment = klass.find(params[:asset_id])
    │
    ├─ entity.attach!(@attachment)
    │   └─ Account#attach!
    │       └─ send(attachment.class.name.tableize) << attachment
    │
    ├─ entity.reload
    │
    └─ respond_with(entity)

EntitiesController#discard
    │
    ├─ load_and_authorize_resource (Account.find)
    │
    ├─ find_class(params[:attachment])
    │   └─ ActiveRecord::Base.descendants から検索
    │
    ├─ @attachment = klass.find(params[:attachment_id])
    │
    ├─ entity.discard!(@attachment)
    │   └─ Account#discard!
    │       ├─ 【Task】attachment.update_attribute(:asset, nil)
    │       └─ 【Other】send(attachment.class.name.tableize).delete(attachment)
    │
    ├─ entity.reload
    │
    └─ respond_with(entity)
```

### データフロー図

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

URLパラメータ ───▶ EntitiesController#attach ───▶ @account
(id,assets,           │                           @attachment
 asset_id)            │                           @attached
                      │                               │
                      ├─ find_class                  │
                      ├─ find(@asset_id)             │
                      ├─ attach!                     │
                      └─ reload                      │
                      │                               ▼
                      ▼                        attach.js.haml
               データベース
               (account_contacts
                or account_opportunities
                or tasks)

URLパラメータ ───▶ EntitiesController#discard ───▶ @account
(id,attachment,       │                            @attachment
 attachment_id)       │                                │
                      │                                │
                      ├─ find_class                   │
                      ├─ find(@attachment_id)         │
                      ├─ discard!                     │
                      └─ reload                       │
                      │                                ▼
                      ▼                         discard.js.haml
               データベース
               (DELETE or UPDATE)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| entities_controller.rb | `app/controllers/entities_controller.rb` | ソース | attach/discardアクション |
| application_controller.rb | `app/controllers/application_controller.rb` | ソース | find_classメソッド |
| account.rb | `app/models/entities/account.rb` | ソース | attach!/discard!メソッド |
| account_contact.rb | `app/models/entities/account_contact.rb` | ソース | 中間テーブル |
| account_opportunity.rb | `app/models/entities/account_opportunity.rb` | ソース | 中間テーブル |
| attach.js.haml | `app/views/accounts/attach.js.haml` | テンプレート | 紐付け結果JS |
| discard.js.haml | `app/views/accounts/discard.js.haml` | テンプレート | 解除結果JS |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義 |
