# 通知設計書 89-expiration_date_updated_email

## 概要

本ドキュメントは、GitLabにおける「メンバーシップ有効期限変更通知（expiration_date_updated_email）」の設計を定義するものである。グループメンバーの有効期限が変更された際に、対象メンバーに通知メールを送信する。

### 本通知の処理概要

本通知は、グループメンバーシップの有効期限（`expires_at`）が変更または削除された際に、対象のメンバーに対して有効期限の変更を通知するメールを送信する機能である。

**業務上の目的・背景**：メンバーシップの有効期限変更をメンバー本人に通知することで、アクセス権の状況について透明性を確保する。これにより、メンバーは自身のアクセス権がいつまで有効かを把握でき、必要に応じて対応を取ることができる。また、有効期限が削除された（無期限になった）場合もその旨を通知する。

**通知の送信タイミング**：Memberレコードの `expires_at` フィールドが変更された直後（`post_update_hook`）に送信される。管理者がメンバー管理画面で有効期限を変更した際にトリガーされる。

**通知の受信者**：有効期限が変更されたメンバー本人。ただし、グループメンバーのみが対象であり、プロジェクトメンバーには送信されない。また、通知設定で `:mention` レベルが無効化されている場合は送信されない。

**通知内容の概要**：グループ名、新しい有効期限（または有効期限が削除されたこと）、およびグループへのリンクが含まれる。

**期待されるアクション**：受信者は通知を確認し、有効期限の変更内容を把握する。必要に応じて管理者に確認や延長の依頼を行う。

## 通知種別

メール（テキスト形式）

## 送信仕様

### 基本情報

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

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

1. Memberレコードからuserを取得
2. `member.user.notification_email_for(notification_group)` でメールアドレスを取得
3. `member.notifiable?(:mention)` で通知可否を判定

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | Gitlab.config.gitlab.email_from |
| 送信元名称 | Gitlab.config.gitlab.email_display_name |
| 件名 | `Group membership expiration date changed` または `Group membership expiration date removed` |
| 形式 | テキスト形式 |

### 本文テンプレート

```
{greeting}

{expiration_changed_message}

{group_link}
```

※ テンプレートはヘルパーメソッド（`say_hi`, `group_membership_expiration_changed_text`, `group_membership_expiration_changed_link`）を使用

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| member | メンバーオブジェクト | params[:member] | Yes |
| member_source | グループ | member.source | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| モデルコールバック | expires_at変更後 | Groupメンバー＆notifiable?(:mention) | post_update_hook |
| 画面操作 | 有効期限変更 | Groupメンバー＆notifiable?(:mention) | メンバー管理画面での変更 |
| API | Member更新（expires_at変更） | Groupメンバー＆notifiable?(:mention) | API経由での更新 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| member.blank? | Memberレコードが存在しない |
| !member_source.is_a?(Group) | プロジェクトメンバーの場合 |
| notifiable?(:mention) = false | 通知設定で無効化されている |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[Member更新 - expires_at変更] --> B{saved_change_to_expires_at?}
    B -->|No| C[送信スキップ]
    B -->|Yes| D[ExpirationDateUpdatedMailer.email]
    D --> E{valid_to_email?}
    E -->|No - member.blank?| F[ログ出力・終了]
    E -->|No - not Group| G[ログ出力・終了]
    E -->|No - not notifiable?| H[ログ出力・終了]
    E -->|Yes| I[mail_with_locale]
    I --> J[deliver_later]
    J --> K[Sidekiqジョブキュー]
    K --> L[メール送信完了]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| members | メンバー情報取得 | expires_at |
| users | ユーザー情報取得 | 送信先メールアドレス |
| namespaces | グループ情報 | source_type='Namespace'の場合のみ |
| notification_settings | 通知設定確認 | notifiable?判定用 |

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

#### members

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | メンバー識別 | paramsで渡される |
| user_id | ユーザーID | - |
| source_id | ソースID | - |
| source_type | ソースタイプ | 'Namespace'（グループ）のみ対象 |
| expires_at | 有効期限日 | 変更検出用 |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| なし | - | 本通知では更新処理なし |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| メンバー未検出 | member.blank? | ログ出力してスキップ |
| グループ以外 | !member_source.is_a?(Group) | ログ出力してスキップ |
| 通知無効 | !notifiable?(:mention) | ログ出力してスキップ |
| SMTP接続エラー | メールサーバー接続失敗 | Sidekiqリトライ |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | Sidekiqデフォルト（25回） |
| リトライ間隔 | 指数バックオフ |
| リトライ対象エラー | SMTPConnectionError等のネットワークエラー |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | Sidekiqキューの設定に依存 |
| 1日あたり上限 | 特に制限なし |

### 配信時間帯

特に制限なし（有効期限変更時に即座に送信）

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

- 受信者は有効期限が変更されたメンバー本人のみ
- グループの詳細情報は含まれない

## 備考

- プロジェクトメンバーには送信されない（グループメンバーのみ）
- `member.expires?` で有効期限の有無を判定（件名の分岐に使用）
- ヘルパーメソッド（`members_helper.rb`）を使用してテンプレートを生成
- レイアウトは標準の `mailer` レイアウトを使用
- `post_update_hook` で `saved_change_to_expires_at?` をチェック

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | member.rb | `app/models/member.rb` | post_update_hook（753-762行目）を確認 |

**主要処理フロー**:
- **758-759行目**: saved_change_to_expires_at?チェックとメーラー呼び出し

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | expiration_date_updated_mailer.rb | `app/mailers/members/expiration_date_updated_mailer.rb` | emailメソッド（11-18行目）を確認 |

**主要処理フロー**:
- **12行目**: valid_to_email?チェック
- **14-17行目**: mail_with_locale呼び出し

#### Step 3: バリデーションを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | expiration_date_updated_mailer.rb | `app/mailers/members/expiration_date_updated_mailer.rb` | valid_to_email?（24-41行目）を確認 |

**主要処理フロー**:
- **25-28行目**: member.blank?チェック
- **30-33行目**: is_a?(Group)チェック
- **35-38行目**: notifiable?(:mention)チェック

#### Step 4: 件名決定ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | expiration_date_updated_mailer.rb | `app/mailers/members/expiration_date_updated_mailer.rb` | email_subject_text（55-61行目）を確認 |

**主要処理フロー**:
- **56-60行目**: member.expires?による件名分岐

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | email.text.erb | `app/views/members/expiration_date_updated_mailer/email.text.erb` | テキストテンプレートの内容確認 |

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

```
Member#after_update
    │
    └─ post_update_hook
           │
           ├─ saved_change_to_access_level? → send_access_level_updated_notification
           │
           └─ saved_change_to_expires_at?
                  │
                  └─ run_after_commit
                         │
                         └─ Members::ExpirationDateUpdatedMailer
                                .with(member:, member_source_type:)
                                .email
                                │
                                ├─ valid_to_email? チェック
                                │      ├─ member.blank?
                                │      ├─ is_a?(Group)
                                │      └─ notifiable?(:mention)
                                │
                                ├─ email_subject_text
                                │      └─ member.expires? 分岐
                                │
                                └─ mail_with_locale → deliver_later
```

### データフロー図

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

Member更新         ───▶ post_update_hook           ───▶ expires_at変更検出
(expires_at変更)         │
                         ▼
                  ExpirationDateUpdatedMailer.email
                         │
                         ├─▶ valid_to_email? チェック
                         │      ├─▶ Groupメンバーか?
                         │      └─▶ notifiable?
                         │
                         ├─▶ email_subject_text
                         │      └─▶ expires? で件名分岐
                         │
                         └─▶ メール送信（Sidekiq）
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| expiration_date_updated_mailer.rb | `app/mailers/members/expiration_date_updated_mailer.rb` | ソース | 有効期限変更通知メーラー |
| email.text.erb | `app/views/members/expiration_date_updated_mailer/email.text.erb` | テンプレート | テキスト形式メールテンプレート |
| member.rb | `app/models/member.rb` | ソース | Memberモデル、post_update_hook定義 |
| members_helper.rb | `app/helpers/members_helper.rb` | ソース | テンプレートヘルパー |
