# 通知設計書 32-unapproved_merge_request_email

## 概要

本ドキュメントは、GitLabにおけるMerge Request承認取り消し通知（unapproved_merge_request_email）の設計を記述したものである。

### 本通知の処理概要

**業務上の目的・背景**：コードレビュープロセスにおいて、一度承認したMerge Requestの承認を取り消す必要が生じることがある。例えば、追加のコミットにより再レビューが必要になった場合や、承認者が誤って承認した場合などである。承認取り消しを関係者に通知することで、マージ可否状態の変化を迅速に把握でき、不正なマージを防止できる。

**通知の送信タイミング**：ユーザーがMerge Requestの承認を取り消した（Unapprove/Revoke approval）時点で送信される。NotificationService#unapprove_mrを経由してメールが送信される。

**通知の受信者**：Merge Requestに関連する以下のユーザーが受信者となる。作成者（Author）、アサイン先（Assignees）、レビュアー（Reviewers）、およびカスタム通知設定で「unapprove」アクションを有効にしているプロジェクトメンバー。受信者はNotificationRecipients::BuildServiceによって決定される。

**通知内容の概要**：承認が取り消されたMerge Requestの情報（タイトル、参照番号、プロジェクト、ブランチ、作成者）、承認取り消しを行ったユーザーの情報、および現在の承認状況（EE版では承認数/必要承認数）が含まれる。

**期待されるアクション**：受信者はMerge Requestの承認状況が変化したことを認識し、必要に応じて再度レビューを依頼するか、自身で再承認を行う。マージ予定だった場合は、承認状況を再確認してからマージを実行する。

## 通知種別

メール

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（deliver_later） |
| 優先度 | 中 |
| リトライ | Sidekiqのデフォルト設定に従う |

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

NotificationRecipients::BuildService.build_recipientsを使用し、action: 'unapprove'として受信者を決定する。具体的には以下の条件でフィルタリングされる：

1. Merge Requestの作成者、アサイン先、レビュアー
2. プロジェクトのwatch設定を持つユーザー
3. カスタム通知設定で承認取り消し通知を有効にしているユーザー
4. 通知が無効化されていないユーザー（Disabled以外）

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | GitLab設定のデフォルト送信元 |
| 送信元名称 | {取り消し者名} (@{取り消し者ユーザー名}) |
| 件名 | Re: {プロジェクト名} \| {MRタイトル} ({MR参照番号}) |
| 形式 | HTML/テキスト（両形式） |

### 本文テンプレート

**HTML版：**
```
Merge request was unapproved (x/y) ※EE版のみ承認数表示

Merge request {MR参照番号} was unapproved by {取り消し者アバター} {取り消し者名}

Project: {ネームスペース}/{プロジェクト名}
Branch: {ソースブランチ}
Author: {作成者アバター} {作成者名}
Assignees: {アサイン先一覧}
Reviewers: {レビュアー一覧}

[View merge request] ボタン
```

**テキスト版：**
```
Merge request {MR参照番号} was unapproved by {取り消し者名}

Merge request URL: {MR URL}

{ソースブランチ} -> {ターゲットブランチ}

Author: {作成者名}
Assignees: {アサイン先一覧}
Reviewers: {レビュアー一覧}
```

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| なし | - | - | 添付ファイルなし |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| @merge_request | Merge Requestオブジェクト | MergeRequest.find(merge_request_id) | Yes |
| @unapproved_by | 承認取り消し者ユーザー | User.find(unapproved_by_user_id) | Yes |
| @project | プロジェクト | @merge_request.project | Yes |
| @target_url | MRのURL | project_merge_request_url | Yes |
| @recipient | 受信者ユーザー | User.find(recipient_id) | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | Merge Request承認取り消しボタンクリック | 承認取り消しが正常に処理された場合 | WebUI経由での承認取り消し |
| API | Merge Request Unapprove API呼び出し | 承認取り消しが正常に処理された場合 | API経由での承認取り消し |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| プロジェクトでメール無効 | project.emails_disabled?がtrueの場合 |
| 受信者の通知設定がDisabled | ユーザーが通知を無効化している場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[Merge Request承認取り消し] --> B[承認レコード削除]
    B --> C[NotificationService#unapprove_mr]
    C --> D[unapprove_mr_email]
    D --> E[NotificationRecipients::BuildService]
    E --> F[受信者リスト生成]
    F --> G[各受信者にメール送信]
    G --> H[Notify.unapproved_merge_request_email.deliver_later]
    H --> I[Sidekiqでメール送信実行]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| merge_requests | Merge Request情報取得 | 承認取り消し対象のMR |
| users | ユーザー情報取得 | 取り消し者、受信者、作成者 |
| projects | プロジェクト情報取得 | 通知設定の確認 |
| approvals | 承認情報取得 | 承認数の確認（EE） |
| notification_settings | 通知設定取得 | 受信者決定 |
| members | メンバー情報取得 | 受信者決定 |

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

#### merge_requests

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | MR識別 | merge_request_id |
| title | メール件名・本文 | - |
| iid | 参照番号 | - |
| author_id | 作成者取得 | - |
| source_branch | ブランチ情報 | - |
| target_project_id | プロジェクト取得 | - |

#### users

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | ユーザー識別 | unapproved_by_user_id, recipient_id |
| name | 表示名 | - |
| email | 送信先メールアドレス | notification_email_for使用 |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| sent_notifications | INSERT | 送信記録の保存 |

#### 送信ログテーブル

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| INSERT | noteable_type | 'MergeRequest' | - |
| INSERT | noteable_id | merge_request_id | - |
| INSERT | recipient_id | 受信者ID | - |
| INSERT | reply_key | 生成されたキー | メール返信用 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | SMTPエラー | Sidekiqリトライ |
| テンプレートエラー | 変数が見つからない | 例外ログ記録 |
| 宛先不正 | メールアドレス形式エラー | スキップして次の受信者へ |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | Sidekiqデフォルト（25回） |
| リトライ間隔 | 指数バックオフ |
| リトライ対象エラー | 一時的なSMTPエラー |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | ApplicationRateLimiter :notification_emails 設定に従う |
| 1日あたり上限 | 設定なし |

### 配信時間帯

制限なし（即座に送信）

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

- 受信者は通知設定に基づいてフィルタリングされ、権限のないユーザーには送信されない
- メール本文にはMerge Requestの内容が含まれるため、機密情報を含む場合は注意が必要
- Reply-Toヘッダーには暗号化されたキーが含まれ、メール返信によるコメント追加が可能

## 備考

- EE版では現在の承認数/必要承認数が表示される（approvals_required）
- mailerレイアウトは'mailer'が使用される（override_layout_lookup_tableで設定）
- 赤色の背景（#d22f57）で承認取り消しを視覚的に強調

---

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

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

### 推奨読解順序

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

まず、承認に関連するデータ構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | approval.rb | `app/models/approval.rb` | Approvalモデルの構造、削除時の挙動 |
| 1-2 | merge_request.rb | `app/models/merge_request.rb` | MergeRequestモデルとapprovalsの関連 |

**読解のコツ**: Railsのhas_many :approvalsで承認の削除がどのように処理されるかに注目

#### Step 2: 通知サービスを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | notification_service.rb | `app/services/notification_service.rb` | unapprove_mr, unapprove_mr_emailメソッド |

**主要処理フロー**:
- **725-727行目**: `unapprove_mr` - NotificationServiceのエントリーポイント
- **942-948行目**: `unapprove_mr_email` - 実際のメール送信処理
- **943行目**: action: 'unapprove'で受信者フィルタリング

#### Step 3: メーラーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | merge_requests.rb | `app/mailers/emails/merge_requests.rb` | unapproved_merge_request_emailメソッドの実装 |
| 3-2 | notify.rb | `app/mailers/notify.rb` | Notifyクラスの構造、mail_answer_thread |

**主要処理フロー**:
- **197-202行目**: `unapproved_merge_request_email` - メール生成
- **198行目**: `setup_merge_request_mail` - 共通セットアップ
- **200行目**: `@unapproved_by = User.find(unapproved_by_user_id)` - 取り消し者取得
- **201行目**: `mail_answer_thread` - スレッド形式でメール送信

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | unapproved_merge_request_email.html.haml | `app/views/notify/unapproved_merge_request_email.html.haml` | HTML版テンプレート構造、赤色警告表示 |
| 4-2 | unapproved_merge_request_email.text.haml | `app/views/notify/unapproved_merge_request_email.text.haml` | テキスト版テンプレート |

**読解のコツ**:
- 背景色が#d22f57（赤）になっていることに注目
- @unapproved_by変数の使用箇所を確認

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

```
承認取り消しアクション
    │
    └─ Approval.destroy
           │
           └─ NotificationService#unapprove_mr
                  │
                  └─ unapprove_mr_email
                         │
                         ├─ NotificationRecipients::BuildService.build_recipients
                         │      └─ action: 'unapprove'
                         │
                         └─ Notify#unapproved_merge_request_email
                                │
                                ├─ setup_merge_request_mail
                                │      └─ SentNotification.record
                                │
                                └─ mail_answer_thread
                                       └─ deliver_later (Sidekiq)
```

### データフロー図

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

merge_request_id ───┬──▶ 承認削除処理 ───▶ Approval削除 (DB)
                    │         │
current_user ───────┤         ▼
                    │  NotificationService#unapprove_mr
                    │         │
                    │         ▼
                    │  NotificationRecipients::BuildService
                    │         │
                    │         ▼
                    └──▶ Notify#unapproved_merge_request_email ───▶ Email (SMTP)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| notification_service.rb | `app/services/notification_service.rb` | ソース | 通知処理サービス |
| merge_requests.rb | `app/mailers/emails/merge_requests.rb` | ソース | MR関連メーラー |
| notify.rb | `app/mailers/notify.rb` | ソース | メインメーラークラス |
| unapproved_merge_request_email.html.haml | `app/views/notify/unapproved_merge_request_email.html.haml` | テンプレート | HTML版メールテンプレート |
| unapproved_merge_request_email.text.haml | `app/views/notify/unapproved_merge_request_email.text.haml` | テンプレート | テキスト版メールテンプレート |
| build_service.rb | `app/services/notification_recipients/build_service.rb` | ソース | 受信者決定サービス |
