# 通知設計書 2-エンティティ割当通知

## 概要

本ドキュメントは、Fat Free CRMにおけるエンティティ割当通知機能の設計を記載する。Account、Contact、Lead、Opportunityが他のユーザーに割り当てられた際、割当先ユーザーにメール通知を送信する機能について定義する。

### 本通知の処理概要

本通知は、CRMシステム内の主要エンティティ（Account、Contact、Lead、Opportunity）が特定のユーザーに割り当てられた際、その担当者に自動的にメール通知を送信する機能である。

**業務上の目的・背景**：営業チームやカスタマーサポートチームにおいて、案件や顧客の担当者割り当ては日常的に発生する業務である。新規案件の割り当て、担当者変更、チーム間の引き継ぎなど、様々な場面でエンティティの担当者が変更される。この通知機能により、新しく担当となったユーザーは即座にその情報を受け取り、迅速に対応を開始することができる。これにより、情報伝達の遅延による顧客対応の遅れを防ぎ、チーム全体の生産性向上に寄与する。

**通知の送信タイミング**：エンティティの作成時（after_create）または更新時（after_update）にassigned_toフィールドが設定・変更された場合に非同期で送信される。ただし、自分自身に割り当てた場合は通知されない。

**通知の受信者**：エンティティのassigneeとして設定されたユーザー（entity.assignee）が受信者となる。ただし、receive_assigned_notifications?設定がオンのユーザーのみに送信される。

**通知内容の概要**：メール件名には「Fat Free CRM: You have been assigned」に続けてエンティティ名とエンティティ種別が含まれる。本文には割当実行者名、割当先エンティティ名、種別、およびエンティティ詳細画面へのURLが含まれる。

**期待されるアクション**：受信者はメール本文に記載されたリンクからエンティティ詳細画面に直接アクセスし、割り当てられた案件・顧客の詳細を確認した上で、必要な対応（連絡、フォローアップ等）を開始することが期待される。

## 通知種別

メール通知

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（deliver_later） |
| 優先度 | 通常 |
| リトライ | Active Jobのデフォルト設定に依存 |

### 送信先決定ロジック

1. エンティティ（Account、Contact、Lead、Opportunity）のassignee関連を取得
2. 割当先ユーザーが以下の条件をすべて満たす場合に送信
   - assigneeが存在する（item.assignee.present?）
   - `receive_assigned_notifications?`がtrueを返す
   - current_userが存在する（割当実行者が特定できる）
   - Setting.hostが設定されている（メール内URLの生成に必要）
3. 割当実行者（current_user）と割当先（assignee）が同一でない場合のみ送信

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | Setting.dig(:smtp, :from) または "Fat Free CRM <noreply@fatfreecrm.com>" |
| 送信元名称 | Fat Free CRM（デフォルト） |
| 件名 | Fat Free CRM: You have been assigned {entity_name} {entity_type} |
| 形式 | HTML（text/html） |

### 本文テンプレート

```
Your colleague {assigner_name} has assigned the {entity_name} {entity_type} to you.
{entity_url}
```

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| なし | - | - | 本通知には添付ファイルはない |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| @entity_url | エンティティ詳細画面URL | url_for(entity) | Yes |
| @entity_name | エンティティ名 | entity.name | Yes |
| @entity_type | エンティティ種別名 | entity.class.name | Yes |
| @assigner_name | 割当実行者名 | assigner.name | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| オブザーバー | Account/Contact/Lead/Opportunity after_create | assigneeが設定され、current_userと異なる | 新規作成時に担当者を設定した場合 |
| オブザーバー | Account/Contact/Lead/Opportunity after_update | assigned_toが変更され、assigneeがcurrent_userと異なる | 既存エンティティの担当者を変更した場合 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 自己割当 | current_user == item.assigneeの場合は送信しない |
| assignee未設定 | assigneeがnilの場合は送信しない |
| receive_assigned_notifications?がfalse | ユーザー設定で割当通知を無効にしている場合 |
| current_user未特定 | PaperTrail.request.whodunnitが設定されていない場合 |
| Setting.host未設定 | メールURL生成に必要なホスト設定がない場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[エンティティ作成/更新] --> B{EntityObserver}
    B --> C{after_create or after_update}
    C -->|after_create| D{current_user != assignee?}
    C -->|after_update| E{assigned_to変更?}
    E -->|No| Z[終了]
    E -->|Yes| F{assignee != current_user?}
    D -->|No| Z
    D -->|Yes| G[send_notification_to_assignee]
    F -->|No| Z
    F -->|Yes| G
    G --> H{assignee.present?}
    H -->|No| Z
    H -->|Yes| I{receive_assigned_notifications?}
    I -->|No| Z
    I -->|Yes| J{current_user.present?}
    J -->|No| Z
    J -->|Yes| K{can_send_email?}
    K -->|No| Z
    K -->|Yes| L[UserMailer.assigned_entity_notification.deliver_later]
    L --> M[メール送信キューに追加]
    M --> N[Active Jobによる非同期送信]
    N --> Z
```

## データベース参照・更新仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| accounts | Accountエンティティ情報 | assigned_to、name |
| contacts | Contactエンティティ情報 | assigned_to、first_name、last_name |
| leads | Leadエンティティ情報 | assigned_to、first_name、last_name |
| opportunities | Opportunityエンティティ情報 | assigned_to、name |
| users | 割当先ユーザー情報 | email、receive_assigned_notifications |
| settings | ホスト設定 | Settingモデル経由 |

### テーブル別参照項目詳細

#### accounts / opportunities

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | エンティティ識別 | - |
| name | エンティティ名 | 件名・本文表示用 |
| assigned_to | 割当先ユーザーID | assignee関連の参照元 |

#### contacts / leads

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | エンティティ識別 | - |
| first_name, last_name | エンティティ名（full_name） | 件名・本文表示用 |
| assigned_to | 割当先ユーザーID | assignee関連の参照元 |

#### users

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | ユーザー識別 | assigned_to値 |
| email | 送信先メールアドレス | - |
| first_name, last_name | 割当実行者名、割当先名 | - |
| receive_assigned_notifications | 通知設定 | 購読オプトイン |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| なし | - | 本通知処理ではデータベース更新は行わない |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | SMTPサーバー接続エラー | Active Jobのリトライ機構に委譲 |
| URL生成エラー | Setting.hostが未設定 | can_send_email?でフィルタリング |
| 宛先不正 | emailカラムが不正な形式 | 標準例外処理（ログ出力） |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | Active Jobのデフォルト設定（通常3回） |
| リトライ間隔 | 指数バックオフ |
| リトライ対象エラー | 一時的なネットワークエラー、SMTPサーバーエラー |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 設定なし（システムのSMTP制限に依存） |
| 1日あたり上限 | 設定なし |

### 配信時間帯

送信時間帯の制限はなし。エンティティの作成・更新時に即座にキューに追加され、非同期で送信される。

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

- メール本文にはエンティティ名とURLのみが含まれ、詳細な顧客情報は含まれない
- 送信先メールアドレスはデータベースに保存されたアドレスのみ
- PaperTrailによるwhodunnit追跡でcurrent_userを特定するため、APIアクセス時の認証が前提

## 備考

- EntityObserverはAccount、Contact、Lead、Opportunityの4種類のエンティティを監視対象としている
- CampaignやTaskは監視対象外のため、割当通知は送信されない
- current_userはPaperTrail.request.whodunnitから取得されるため、バッチ処理等でwhodunnitが設定されていない場合は通知されない

---

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

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

### 推奨読解順序

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

エンティティとユーザーの関連、assigned_toフィールドの役割を理解することが最初のステップである。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | account.rb | `app/models/entities/account.rb` | assigned_to関連、belongs_to :assigneeの定義 |
| 1-2 | user.rb | `app/models/users/user.rb` | has_many関連、receive_assigned_notifications属性 |

**読解のコツ**: 各エンティティモデル（Account、Contact、Lead、Opportunity）にはbelongs_to :assignee, class_name: 'User', foreign_key: :assigned_toという関連が定義されている。これにより、entity.assigneeでUserオブジェクトを取得できる。

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

EntityObserverがエンティティの作成・更新を監視し、通知をトリガーする。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | entity_observer.rb | `app/models/observers/entity_observer.rb` | observe宣言、after_create、after_updateコールバック |

**主要処理フロー**:
1. **9行目**: `observe :account, :contact, :lead, :opportunity` - 監視対象のモデルを指定
2. **11-13行目**: `after_create` - 新規作成時にcurrent_user != assigneeなら通知
3. **15-17行目**: `after_update` - assigned_toが変更され、assignee != current_userなら通知

#### Step 3: 通知送信判定ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | entity_observer.rb | `app/models/observers/entity_observer.rb` | send_notification_to_assignee、can_send_email?メソッド |

**主要処理フロー**:
- **21-23行目**: `send_notification_to_assignee` - 4つの条件（assignee存在、receive_assigned_notifications?、current_user存在、can_send_email?）をすべて満たす場合のみ送信
- **26-28行目**: `can_send_email?` - Setting.hostが設定されているか確認
- **30-37行目**: `current_user` - PaperTrail.request.whodunnitからユーザーを取得

#### Step 4: メーラーとテンプレートを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | user_mailer.rb | `app/mailers/user_mailer.rb` | assigned_entity_notificationメソッド |
| 4-2 | assigned_entity_notification.html.haml | `app/views/user_mailer/assigned_entity_notification.html.haml` | メールテンプレート |

**主要処理フロー**:
- **9-17行目（user_mailer.rb）**: インスタンス変数の設定、件名の組み立て、送信先・送信元の設定
- **1-3行目（template）**: プレーンテキスト形式での本文生成

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

```
AccountsController#create / #update（または他のエンティティコントローラー）
    │
    └─ Account.create / Account.update
            │
            └─ EntityObserver
                   │
                   ├─ [after_create]
                   │      └─ current_user != item.assignee?
                   │              └─ send_notification_to_assignee
                   │
                   └─ [after_update]
                          └─ item.saved_change_to_assigned_to?
                                 └─ item.assignee != current_user?
                                        └─ send_notification_to_assignee
                                               │
                                               ├─ item.assignee.present?
                                               ├─ item.assignee.receive_assigned_notifications?
                                               ├─ current_user.present?
                                               └─ can_send_email? (Setting.host.present?)
                                                      │
                                                      └─ UserMailer.assigned_entity_notification(item, current_user)
                                                             └─ mail(subject:, to:, from:)
                                                                    └─ assigned_entity_notification.html.haml
```

### データフロー図

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

Entity.create/update ──▶ EntityObserver.after_create/update
       │                        │
       ├─ assigned_to           ▼
       └─ user_id        current_user != assignee?
                                │
                                ▼
                         saved_change_to_assigned_to?（updateの場合）
                                │
                                ▼
                         send_notification_to_assignee
                                │
                         条件チェック
                         - assignee.present?
                         - receive_assigned_notifications?
                         - current_user.present?
                         - Setting.host.present?
                                │
                                ▼
                         UserMailer.assigned_entity_notification
                                │
                                ▼
                         ActiveJob Queue ──────────────────▶ SMTP Server ──▶ Email
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| entity_observer.rb | `app/models/observers/entity_observer.rb` | ソース | オブザーバー、通知トリガー、条件判定 |
| user_mailer.rb | `app/mailers/user_mailer.rb` | ソース | メーラークラス、メール生成ロジック |
| assigned_entity_notification.html.haml | `app/views/user_mailer/assigned_entity_notification.html.haml` | テンプレート | メール本文テンプレート |
| user.rb | `app/models/users/user.rb` | ソース | ユーザーモデル、receive_assigned_notifications属性 |
| account.rb | `app/models/entities/account.rb` | ソース | Accountエンティティ、assignee関連 |
| contact.rb | `app/models/entities/contact.rb` | ソース | Contactエンティティ、assignee関連 |
| lead.rb | `app/models/entities/lead.rb` | ソース | Leadエンティティ、assignee関連 |
| opportunity.rb | `app/models/entities/opportunity.rb` | ソース | Opportunityエンティティ、assignee関連 |
| action_mailer.rb | `config/initializers/action_mailer.rb` | 設定 | ActionMailer初期化、SMTP設定 |
| user_mailer_spec.rb | `spec/mailers/user_mailer_spec.rb` | テスト | メーラーのテスト |
| entity_observer_spec.rb | `spec/models/observers/entity_observer_spec.rb` | テスト | オブザーバーのテスト |
