# 通知設計書 30-merge_requests_csv_email

## 概要

本ドキュメントは、Merge RequestのCSVエクスポートが完了した際に、エクスポートを要求したユーザーへCSVファイルを添付してメール送信する機能の設計を記載する。

### 本通知の処理概要

Merge RequestのCSVエクスポート処理が完了した際に、エクスポートされたCSVファイルを添付ファイルとしてメール送信する機能である。

**業務上の目的・背景**：GitLabでは、プロジェクト内のMerge Request一覧をCSV形式でエクスポートする機能がある。この機能により、ユーザーはMerge Requestのデータを外部ツールで分析したり、レポート作成に活用したりできる。エクスポート処理は非同期で実行されるため、完了時にメール通知と共にCSVファイルを配信することで、ユーザーは処理完了を待たずに他の作業を継続できる。

**通知の送信タイミング**：MergeRequests::ExportCsvServiceのemailメソッドが呼び出された時点で、即座に同期送信（deliver_now）される。

**通知の受信者**：CSVエクスポートを要求したユーザー本人のみが受信対象となる。

**通知内容の概要**：エクスポートされたMerge Request数、プロジェクト名、プロジェクトURL、エクスポートされたCSVファイル（添付）が含まれる。トランケーションが発生した場合は、その旨と件数の警告も含まれる。

**期待されるアクション**：受信者は、添付されたCSVファイルをダウンロードして、必要な分析やレポート作成に利用することが期待される。

## 通知種別

メール

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期（deliver_now） |
| 優先度 | 高（ユーザーの明示的なエクスポート要求への応答） |
| リトライ | なし（同期送信のため） |

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

CSVエクスポートを要求したユーザー（user）に直接送信する。NotificationRecipients::BuildServiceは使用せず、MergeRequests::ExportCsvServiceから直接ユーザーが指定される。

送信先メールアドレスは `user.notification_email_for(@project.group)` で決定される。

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | GitLabの設定に基づくシステムメールアドレス |
| 送信元名称 | システムデフォルト |
| 件名 | {project_name} \| Exported merge requests |
| 形式 | HTML/テキスト両対応 |
| レイアウト | mailer（デフォルト） |

### 本文テンプレート

**テキスト形式**:
```
Your CSV export of {written_count} merge requests from project {project_name} ({project_url}) has been added to this email as an attachment.

（トランケーション時のみ追加）
This attachment has been truncated to avoid exceeding the maximum allowed attachment size of {size_limit}. {written_count} of {count} merge requests have been included. Consider re-exporting with a narrower selection of merge requests.
```

**HTML形式**:
テキスト形式と同様の内容を、HTMLフォーマットで表示。プロジェクト名はリンク形式で表示される。

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| {project_path}_merge_requests_{date}.csv | text/csv | 常に添付 | エクスポートされたMerge Request一覧 |

ファイル名の形式: `{project.full_path.parameterize}_merge_requests_{Date.current.iso8601}.csv`

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| @project | 対象プロジェクト | MergeRequests::ExportCsvServiceから渡される | Yes |
| @count | エクスポート予定のMR件数 | export_status.fetch(:rows_expected) | Yes |
| @written_count | 実際に書き込まれたMR件数 | export_status.fetch(:rows_written) | Yes |
| @truncated | トランケーションの有無 | export_status.fetch(:truncated) | Yes |
| @size_limit | 最大添付ファイルサイズ（人間可読形式） | ExportCsv::BaseService::TARGET_FILESIZE | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | Merge Request一覧でCSVエクスポートボタン押下 | エクスポート処理完了時 | UIからのCSVエクスポート要求 |
| API | CSVエクスポートAPI | エクスポート処理完了時 | REST API経由でのエクスポート要求 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| なし | エクスポート要求者には常に送信される |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[CSVエクスポート要求] --> B[MergeRequests::ExportCsvService]
    B --> C[csv_data生成]
    C --> D[CsvBuilder.render]
    D --> E{TARGET_FILESIZE超過?}
    E -->|Yes| F[トランケーション実行]
    E -->|No| G[完全データ出力]
    F --> H[export_status設定]
    G --> H
    H --> I[email メソッド呼び出し]
    I --> J[Notify.merge_requests_csv_email]
    J --> K[@project, @count, @written_count, @truncated, @size_limit セット]
    K --> L[CSVファイル添付]
    L --> M[email_with_layout]
    M --> N[deliver_now]
    N --> O[終了]
```

## CSVエクスポート項目

| カラム名 | 説明 | データ取得元 |
|---------|------|-------------|
| Title | MRタイトル | merge_request.title |
| Description | MR説明 | merge_request.description |
| MR IID | MRのプロジェクト内ID | merge_request.iid |
| URL | MRのURL | merge_request_url(merge_request) |
| State | MRの状態 | merge_request.state |
| Source Branch | ソースブランチ | merge_request.source_branch |
| Target Branch | ターゲットブランチ | merge_request.target_branch |
| Source Project ID | ソースプロジェクトID | merge_request.source_project_id |
| Target Project ID | ターゲットプロジェクトID | merge_request.target_project_id |
| Author | 作成者名 | merge_request.author.name |
| Author Username | 作成者ユーザー名 | merge_request.author.username |
| Assignees | アサイン先名（カンマ区切り） | merge_request.assignees.map(&:name).join(', ') |
| Assignee Usernames | アサイン先ユーザー名（カンマ区切り） | merge_request.assignees.map(&:username).join(', ') |
| Approvers | 承認者名（カンマ区切り） | merge_request.approved_by_users.map(&:name).join(', ') |
| Approver Usernames | 承認者ユーザー名（カンマ区切り） | merge_request.approved_by_users.map(&:username).join(', ') |
| Merged User | マージ実行者名 | merge_request.metrics&.merged_by&.name |
| Merged Username | マージ実行者ユーザー名 | merge_request.metrics&.merged_by&.username |
| Milestone ID | マイルストーンID | merge_request.milestone&.id |
| Created At (UTC) | 作成日時（UTC） | merge_request.created_at.utc |
| Updated At (UTC) | 更新日時（UTC） | merge_request.updated_at.utc |

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| merge_requests | エクスポート対象MR取得 | 検索条件に基づく |
| users | 作成者・アサイン先・承認者情報 | 各MRに紐づく |
| projects | プロジェクト情報 | resource_parent |
| merge_request_metrics | マージ情報 | merged_by取得 |
| milestones | マイルストーン情報 | milestone_id取得 |

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

#### merge_requests

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | MR識別 | フィルタ条件に基づく |
| title | CSVカラム出力 | - |
| description | CSVカラム出力 | - |
| iid | CSVカラム出力 | - |
| state | CSVカラム出力 | - |
| source_branch | CSVカラム出力 | - |
| target_branch | CSVカラム出力 | - |
| source_project_id | CSVカラム出力 | - |
| target_project_id | CSVカラム出力 | - |
| author_id | 作成者取得 | - |
| created_at | CSVカラム出力 | - |
| updated_at | CSVカラム出力 | - |

### 更新テーブル一覧

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

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | SMTPエラー | 例外発生（同期送信のため） |
| データ取得失敗 | MRデータ取得エラー | エクスポート処理中断 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | なし（同期送信） |
| リトライ間隔 | - |
| リトライ対象エラー | - |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | Gitlab::ApplicationRateLimiter設定に準拠 |
| 1日あたり上限 | 設定なし |

### 配信時間帯

制限なし（エクスポート処理完了時に即座に配信）

### 添付ファイルサイズ制限

| 項目 | 内容 |
|-----|------|
| 最大サイズ | 15MB（TARGET_FILESIZE） |
| 超過時の処理 | トランケーション（件数を減らして出力） |

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

- 受信者はCSVエクスポートを要求したユーザー本人のみに限定される
- エクスポートされるデータはユーザーが閲覧権限を持つMerge Requestのみ
- CSVファイルにはMerge Requestの詳細情報が含まれるため、メールの取り扱いに注意が必要
- 添付ファイルサイズは15MBに制限され、超過時はトランケーションされる

## 備考

- 本通知は他のMR関連通知と異なり、NotificationRecipients::BuildServiceを使用しない
- 送信方式はdeliver_nowによる同期送信であり、Sidekiqキューは使用しない
- mail_answer_threadではなくemail_with_layoutを使用する（スレッド形式ではない）
- CSVエクスポート項目はheader_to_value_hashで定義されている
- トランケーション発生時は、ユーザーに再エクスポートを促すメッセージが表示される
- Issues CSVエクスポート（issues_csv_email）と同様のパターンで実装されている

---

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

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

### 推奨読解順序

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

まず、CSVエクスポートの対象となるMerge Requestの構造と出力項目を理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | merge_request.rb | `app/models/merge_request.rb` | MergeRequestモデルの基本構造 |
| 1-2 | merge_request_metrics.rb | `app/models/merge_request/metrics.rb` | マージ情報（merged_by等） |

**読解のコツ**: CSVに出力される各カラムがMergeRequestモデルのどの属性・関連に対応するかを理解する。

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

処理の起点となるExportCsvServiceを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | export_csv_service.rb | `app/services/merge_requests/export_csv_service.rb` | MR専用のCSVエクスポートサービス（1-39行目） |
| 2-2 | base_service.rb | `app/services/export_csv/base_service.rb` | 共通のCSVエクスポート基底クラス（1-56行目） |

**主要処理フロー**:
1. **export_csv_service.rb 8-10行目**: emailメソッドでNotify.merge_requests_csv_emailを呼び出し
2. **export_csv_service.rb 14-36行目**: header_to_value_hashでCSVカラム定義
3. **base_service.rb 6行目**: TARGET_FILESIZE = 15.megabytes
4. **base_service.rb 14-16行目**: csv_dataメソッドでCSVBuilder.renderを呼び出し

#### Step 3: メール送信処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | merge_requests.rb | `app/mailers/emails/merge_requests.rb` | merge_requests_csv_emailメソッド（175-188行目） |
| 3-2 | notify.rb | `app/mailers/notify.rb` | email_with_layoutメソッド（276-281行目） |

**主要処理フロー**:
- **175-188行目**: @project, @count, @written_count, @truncated, @size_limitをセット
- **183-184行目**: ファイル名生成とCSV添付
- **185-187行目**: email_with_layoutでメール送信

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | merge_requests_csv_email.text.erb | `app/views/notify/merge_requests_csv_email.text.erb` | テキストメール本文 |
| 4-2 | merge_requests_csv_email.html.haml | `app/views/notify/merge_requests_csv_email.html.haml` | HTMLメール本文 |
| 4-3 | _issuable_csv_export.text.erb | `app/views/notify/_issuable_csv_export.text.erb` | 共通パーシャル（テキスト） |
| 4-4 | _issuable_csv_export.html.haml | `app/views/notify/_issuable_csv_export.html.haml` | 共通パーシャル（HTML） |

**読解のコツ**: merge_requests_csv_email.text.erb/html.hamlは_issuable_csv_exportパーシャルをrenderしている。type: :merge_requestが渡される。

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

```
MergeRequests::ExportCsvService.email(user)
    │
    ├─ csv_data (継承: ExportCsv::BaseService)
    │      ├─ csv_builder.render(TARGET_FILESIZE)
    │      │      ├─ CsvBuilder.new(objects, data_hash, associations)
    │      │      └─ render → csv_data, status
    │      └─ status: {rows_expected, rows_written, truncated}
    │
    └─ Notify.merge_requests_csv_email(user, project, csv_data, export_status)
           ├─ @project = project
           ├─ @count = export_status[:rows_expected]
           ├─ @written_count = export_status[:rows_written]
           ├─ @truncated = export_status[:truncated]
           ├─ @size_limit = number_to_human_size(TARGET_FILESIZE)
           ├─ attachments[filename] = csv_data
           └─ email_with_layout(to: user.notification_email_for, subject: ...)
                  └─ deliver_now
```

### データフロー図

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

User ─────────────────▶ MergeRequests::ExportCsvService
                              │
MergeRequest[] ───────▶ header_to_value_hash ──────────▶ csv_data
                              │
                              ▼
                         CsvBuilder.render(15MB)
                              │
                              ├─ rows_expected
                              ├─ rows_written
                              ├─ truncated
                              ▼
                         Notify.merge_requests_csv_email
                              │
                              ├─ @project
                              ├─ @count, @written_count
                              ├─ @truncated, @size_limit
                              ├─ attachments[csv_file]
                              ▼
                         email_with_layout ─────────────▶ Email送信（同期）
                              │
                              └─ テンプレート: _issuable_csv_export
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| export_csv_service.rb | `app/services/merge_requests/export_csv_service.rb` | ソース | MR専用CSVエクスポートサービス |
| base_service.rb | `app/services/export_csv/base_service.rb` | ソース | CSVエクスポート基底クラス |
| merge_requests.rb | `app/mailers/emails/merge_requests.rb` | ソース | merge_requests_csv_emailメソッド定義 |
| notify.rb | `app/mailers/notify.rb` | ソース | メーラー基底クラス、email_with_layout |
| merge_requests_csv_email.text.erb | `app/views/notify/merge_requests_csv_email.text.erb` | テンプレート | テキストメール本文 |
| merge_requests_csv_email.html.haml | `app/views/notify/merge_requests_csv_email.html.haml` | テンプレート | HTMLメール本文 |
| _issuable_csv_export.text.erb | `app/views/notify/_issuable_csv_export.text.erb` | パーシャル | CSV通知共通テキストテンプレート |
| _issuable_csv_export.html.haml | `app/views/notify/_issuable_csv_export.html.haml` | パーシャル | CSV通知共通HTMLテンプレート |
| csv_builder.rb | `lib/csv_builder.rb` | ライブラリ | CSV生成ライブラリ |
