# 機能設計書 86-購読/購読解除

## 概要

本ドキュメントは、Fat Free CRMにおける「購読/購読解除」機能の設計仕様を定義する。エンティティの変更通知をメールで受け取るための購読管理機能である。

### 本機能の処理概要

購読/購読解除機能は、ユーザーが関心のあるエンティティ（取引先、キャンペーン、リード、連絡先、商談）の変更通知をメールで受け取るかどうかを制御する機能である。購読することで、他のユーザーによる更新やコメントがあった際に通知を受け取れる。

**業務上の目的・背景**：CRMシステムでは、特定の顧客や商談の進捗を把握することが重要である。しかし、すべてのエンティティの変更通知を受け取ると情報過多になる。購読機能により、ユーザーは自分が注目したいエンティティのみの通知を受け取るよう設定できる。

**機能の利用シーン**：
- 営業マネージャーが部下の重要商談を購読して進捗を追跡する場合
- チームメンバーが共同で管理する取引先の変更を追跡する場合
- 担当変更後も以前担当していた顧客の動向を追跡する場合
- 不要な通知を止めるために購読を解除する場合

**主要な処理内容**：
1. 購読状態の確認：subscribed_usersにユーザーIDが含まれているか確認
2. 購読の追加：subscribed_usersにユーザーIDを追加
3. 購読の解除：subscribed_usersからユーザーIDを削除
4. UI更新：Ajax経由で購読リンクを更新

**関連システム・外部連携**：
- メール通知システム：購読者への変更通知メール送信
- コメント機能：新規コメント時に購読者へ通知

**権限による制御**：ユーザーは自身がアクセス可能なエンティティのみ購読できる。購読状態の変更は本人のみ可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 8 | 取引先詳細画面 | 主画面 | 取引先の購読/購読解除 |
| 12 | キャンペーン詳細画面 | 主画面 | キャンペーンの購読/購読解除 |
| 16 | リード詳細画面 | 主画面 | リードの購読/購読解除 |
| 21 | 連絡先詳細画面 | 主画面 | 連絡先の購読/購読解除 |
| 25 | 商談詳細画面 | 主画面 | 商談の購読/購読解除 |

## 機能種別

通知設定・購読管理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | Integer | Yes | エンティティID | 有効なエンティティID |

### 入力データソース

- 画面入力：詳細画面の購読/購読解除リンクのクリック
- Ajax POSTリクエスト

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| entity.subscribed_users | Array[Integer] | 購読中のユーザーID配列 |

### 出力先

- 画面表示：購読リンクの状態更新
- データベース：エンティティのsubscribed_usersカラム

## 処理フロー

### 処理シーケンス

```
1. 購読/購読解除リクエスト
   └─ POST /:entities/:id/subscribe または /unsubscribe
2. 権限チェック
   └─ CanCanでエンティティへのアクセス権限確認
3. 購読状態の更新
   └─ subscribed_usersにユーザーIDを追加/削除
4. エンティティ保存
   └─ entity.save
5. UI更新
   └─ subscription_update.js.hamlでリンクを更新
```

### フローチャート

```mermaid
flowchart TD
    A[購読/購読解除リンククリック] --> B{アクション?}
    B -->|subscribe| C[subscribed_usersにユーザーID追加]
    B -->|unsubscribe| D[subscribed_usersからユーザーID削除]
    C --> E[entity.save]
    D --> E
    E --> F[subscription_update.js.haml実行]
    F --> G[購読リンクの状態更新]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-86-01 | 購読追加 | subscribed_usersにユーザーIDを追加 | subscribeアクション時 |
| BR-86-02 | 購読解除 | subscribed_usersからユーザーIDを削除 | unsubscribeアクション時 |
| BR-86-03 | 重複防止 | 既に購読中の場合は追加しない | += 演算子で重複排除 |
| BR-86-04 | 自己のみ | ユーザーは自分自身の購読状態のみ変更可能 | 常時 |
| BR-86-05 | 履歴除外 | subscribed_usersの変更はバージョン履歴に記録しない | has_paper_trail ignore設定 |

### 計算ロジック

- **購読追加**: `entity.subscribed_users += [current_user.id]`
- **購読解除**: `entity.subscribed_users -= [current_user.id]`

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 購読/購読解除 | accounts/campaigns/contacts/leads/opportunities | UPDATE | subscribed_usersカラムの更新 |

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

#### 各エンティティテーブル（accounts等）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | subscribed_users | ユーザーID配列（シリアライズ） | 購読/購読解除時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | 認証エラー | 未ログイン状態でのアクセス | ログインページへリダイレクト |
| - | アクセス拒否 | 権限のないエンティティへのアクセス | AccessDenied例外 |

### リトライ仕様

購読処理にリトライ機能はない。

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

購読状態の変更は単一のUPDATE文で処理される。

## パフォーマンス要件

- 購読状態の変更は即時反映
- subscribed_usersはシリアライズされた配列として保存

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

- **認証**：ログインユーザーのみ購読操作可能
- **認可**：自分自身の購読状態のみ変更可能（他ユーザーの購読状態は変更不可）
- **データ保護**：subscribed_usersはバージョン履歴から除外

## 備考

- subscribed_usersは各エンティティモデルでserializeされた配列として保存
- 購読状態の変更はバージョン履歴（PaperTrail）から除外される
- メール通知の送信ロジックは別機能（通知機能）で管理

---

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

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

### 推奨読解順序

#### Step 1: コントローラーの購読/購読解除処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | entities_controller.rb | `app/controllers/entities_controller.rb` | subscribe/unsubscribeアクション |

**主要処理フロー**:
- **42-50行目**: `subscribe`アクション
  - `entity.subscribed_users += [current_user.id]`で購読追加
  - `entity.save`で保存
  - `subscription_update`テンプレートをレンダリング
- **53-61行目**: `unsubscribe`アクション
  - `entity.subscribed_users -= [current_user.id]`で購読解除
  - `entity.save`で保存
  - `subscription_update`テンプレートをレンダリング

**読解のコツ**: `+=`と`-=`演算子がRubyの配列操作であることを理解する。重複は自動的に排除される。

#### Step 2: モデルのsubscribed_users設定を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | account.rb | `app/models/entities/account.rb` | serializeとhas_paper_trail設定 |

**主要処理フロー**:
- **44行目**: `serialize :subscribed_users, type: Array`でシリアライズ設定
- **68行目**: `has_paper_trail ... ignore: [:subscribed_users]`でバージョン履歴から除外

#### Step 3: ビューの購読リンクを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | _subscription_links.html.haml | `app/views/comments/_subscription_links.html.haml` | 購読リンクUI |

**主要処理フロー**:
- **4行目**: `subscribed = entity.subscribed_users.include?(current_user.id)`で購読状態確認
- **6-9行目**: 購読状態に応じてリンクテキストとアクションを切り替え
- **13行目**: Ajax（remote: true）でリンクを生成

#### Step 4: JavaScript更新処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | subscription_update.js.haml | `app/views/entities/subscription_update.js.haml` | Ajax応答でのUI更新 |

**主要処理フロー**:
- **1-2行目**: エンティティクラス名とIDからDOM要素を特定
- **4行目**: jQueryで購読リンク部分のHTMLを更新

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

```
ブラウザ（購読/購読解除リンククリック）
    │
    ├─ Ajax POST Request
    │      └─ /:entities/:id/subscribe または /unsubscribe
    │
    └─ EntitiesController#subscribe / #unsubscribe
           │
           ├─ load_and_authorize_resource
           │      └─ CanCan権限チェック
           │
           ├─ entity.subscribed_users += / -= [current_user.id]
           │
           ├─ entity.save
           │      └─ subscribed_usersカラム更新
           │
           └─ render 'subscription_update'
                  │
                  └─ subscription_update.js.haml
                         │
                         └─ _subscription_links.html.haml
                                │
                                └─ DOM更新（購読リンクHTML置換）
```

### データフロー図

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

subscribeクリック   ──────▶  EntitiesController#subscribe
                                │
                                ▼
                          subscribed_users += [user.id]
                                │
                                ▼
                          entity.save
                                │
                                ▼
[DB]                     subscribed_users UPDATE    ──────▶  シリアライズ配列
                                │
                                ▼
                          subscription_update.js    ──────▶  Ajax応答
                                │
                                ▼
                          _subscription_links.html ──────▶  リンクUI更新


unsubscribeクリック ──────▶  EntitiesController#unsubscribe
                                │
                                ▼
                          subscribed_users -= [user.id]
                                │
                                ▼
                          （以下同様）
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| entities_controller.rb | `app/controllers/entities_controller.rb` | ソース | subscribe/unsubscribeアクション（42-61行目） |
| account.rb | `app/models/entities/account.rb` | ソース | serialize :subscribed_users（44行目） |
| campaign.rb | `app/models/entities/campaign.rb` | ソース | serialize :subscribed_users |
| contact.rb | `app/models/entities/contact.rb` | ソース | serialize :subscribed_users（59行目） |
| lead.rb | `app/models/entities/lead.rb` | ソース | serialize :subscribed_users |
| opportunity.rb | `app/models/entities/opportunity.rb` | ソース | serialize :subscribed_users |
| _subscription_links.html.haml | `app/views/comments/_subscription_links.html.haml` | テンプレート | 購読リンクUI |
| subscription_update.js.haml | `app/views/entities/subscription_update.js.haml` | テンプレート | Ajax応答でのUI更新 |
| routes.rb | `config/routes.rb` | 設定 | subscribe/unsubscribeルーティング（57-58行目等） |
