# 通知設計書 87-about_to_expire_email

## 概要

本ドキュメントは、GitLabにおける「メンバーシップ有効期限通知（about_to_expire_email）」の設計を定義するものである。メンバーシップの有効期限が近づいている場合に、対象メンバーに通知メールを送信する。

### 本通知の処理概要

本通知は、プロジェクトまたはグループのメンバーシップに有効期限が設定されており、その期限が近づいている場合に、対象のメンバーに対して有効期限が迫っていることを通知するメールを送信する機能である。

**業務上の目的・背景**：メンバーシップの有効期限切れによる突然のアクセス喪失を防止し、メンバーが事前に対応を取れるようにする。有効期限が設定されている場合、メンバーは期限前に管理者に延長を依頼するか、必要なデータのバックアップや引き継ぎを行う機会を得られる。

**通知の送信タイミング**：バッチ処理（スケジュールジョブ）により定期的に実行され、有効期限が近づいているメンバーを検出して通知を送信する。`Member.expiring_and_not_notified` スコープで対象メンバーを抽出する。

**通知の受信者**：有効期限が近づいているメンバー本人。通知設定で `:mention` レベルが無効化されている場合は送信されない。

**通知内容の概要**：メンバーシップの残り日数、プロジェクト/グループ名、およびメンバーシップ詳細へのリンクが含まれる。

**期待されるアクション**：受信者は通知を確認し、必要に応じて管理者にメンバーシップの延長を依頼するか、期限切れ前に必要な作業を完了させる。

## 通知種別

メール（テキスト形式）

## 送信仕様

### 基本情報

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

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

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

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | Gitlab.config.gitlab.email_from |
| 送信元名称 | Gitlab.config.gitlab.email_display_name |
| 件名 | `Your membership will expire in {days_to_expire} days` |
| 形式 | テキスト形式 |

### 本文テンプレート

```
{greeting}

{expiration_message}

{membership_link}
```

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

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| user | 対象ユーザー | member.user | Yes |
| member | メンバーオブジェクト | params[:member] | Yes |
| member_source | プロジェクト/グループ | member.source | Yes |
| days_to_expire | 有効期限までの日数 | (member.expires_at - Date.today).to_i | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| スケジュールジョブ | バッチ処理実行 | expires_atが近い＆未通知 | 定期的なバッチ処理 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| member.blank? | Memberレコードが存在しない |
| member.expires_at.blank? | 有効期限が設定されていない |
| days_to_expire <= 0 | すでに期限切れ |
| notifiable?(:mention) = false | 通知設定で無効化されている |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[バッチ処理実行] --> B[expiring_and_not_notified取得]
    B --> C{対象メンバーあり?}
    C -->|No| D[終了]
    C -->|Yes| E[各メンバーに対して処理]
    E --> F{member.blank?}
    F -->|Yes| G[スキップ]
    F -->|No| H{expires_at.blank?}
    H -->|Yes| G
    H -->|No| I{days_to_expire > 0?}
    I -->|No| G
    I -->|Yes| J{notifiable?:mention}
    J -->|No| G
    J -->|Yes| K[AboutToExpireMailer.email]
    K --> L[mail_with_locale]
    L --> M[deliver_later]
    M --> N[expiry_notified_at更新]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| members | メンバー情報取得 | expires_at, expiry_notified_at |
| users | ユーザー情報取得 | 送信先メールアドレス |
| projects | プロジェクト情報 | source_type='Project'の場合 |
| namespaces | グループ/名前空間情報 | source_type='Namespace'の場合 |
| notification_settings | 通知設定確認 | notifiable?判定用 |

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

#### members

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | メンバー識別 | paramsで渡される |
| user_id | ユーザーID | - |
| source_id | ソースID | - |
| source_type | ソースタイプ | - |
| expires_at | 有効期限日 | 期限計算用 |
| expiry_notified_at | 通知済み日時 | NULLで未通知 |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| members | UPDATE | expiry_notified_atの更新 |

#### members（通知後）

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| UPDATE | expiry_notified_at | 現在日時 | 重複送信防止 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| SMTP接続エラー | メールサーバー接続失敗 | Sidekiqリトライ |

### リトライ仕様

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

## 配信設定

### レート制限

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

### 配信時間帯

バッチ処理のスケジュールに依存

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

- 受信者は有効期限が設定されているメンバー本人のみ
- メンバーシップの詳細情報は含まれない

## 備考

- `expiring_and_not_notified` スコープで対象メンバーを抽出
- `days_to_expire` は `(member.expires_at - Date.today).to_i` で計算
- ヘルパーメソッド（`members_helper.rb`）を使用してテンプレートを生成
- レイアウトは標準の `mailer` レイアウトを使用

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | member.rb | `app/models/member.rb` | expires_at, expiry_notified_atフィールドとスコープを確認 |

**読解のコツ**: `expiring_and_not_notified` スコープ（199行目）で対象メンバーを抽出している。

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

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

**主要処理フロー**:
- **12行目**: 複合条件チェック（member.blank?, expires_at.blank?, days_to_expire <= 0）
- **13行目**: notifiable?(:mention)チェック
- **15-18行目**: mail_with_locale呼び出し

#### Step 3: 日数計算を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | about_to_expire_mailer.rb | `app/mailers/members/about_to_expire_mailer.rb` | days_to_expire（30-32行目）を確認 |

**主要処理フロー**:
- **30-32行目**: days_to_expire計算 - `(member.expires_at - Date.today).to_i`

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

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

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

```
バッチ処理（スケジュールジョブ）
    │
    └─ Member.expiring_and_not_notified(date)
           │
           └─ 各メンバーに対して
                  │
                  └─ Members::AboutToExpireMailer.with(member:).email
                         │
                         ├─ member.blank? チェック
                         ├─ expires_at.blank? チェック
                         ├─ days_to_expire <= 0 チェック
                         ├─ notifiable?(:mention) チェック
                         │
                         └─ mail_with_locale → deliver_later
```

### データフロー図

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

バッチ処理       ───▶ expiring_and_not_notified    ───▶ 対象メンバー抽出
                       │
                       ▼
                  AboutToExpireMailer.email
                       │
                       ├─▶ days_to_expire 計算
                       │
                       └─▶ メール送信（Sidekiq）
                              │
                              └─▶ expiry_notified_at 更新
```

### 関連ファイル一覧

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