# バッチ設計書 7-Notification::Bundle::DeliverJob

## 概要

本ドキュメントは、個別の通知バンドルを配信するバックグラウンドジョブ `Notification::Bundle::DeliverJob` の設計仕様を記載したものである。

### 本バッチの処理概要

このバッチは、特定の通知バンドルを受け取り、ユーザーに対してバンドルメール（集約された通知メール）を配信する処理を行う。SMTPエラーに対する堅牢なエラーハンドリングを実装しており、メール配信の信頼性を確保している。

**業務上の目的・背景**：fizzyでは、ユーザーの設定に基づき通知を一定期間集約し、まとめてメールで配信する「バンドル通知」機能がある。これにより、頻繁な通知によるメール過多を防ぎ、ユーザーは効率的に通知内容を確認できる。本ジョブは、`DeliverAllJob`によってキューイングされ、実際のメール配信処理を担当する。

**バッチの実行タイミング**：`Notification::Bundle::DeliverAllJob`から一括キューイングされる、または`deliver_later`メソッドで個別にキューイングされる。

**主要な処理内容**：
1. 配信対象のバンドルを受け取り、配信処理を開始
2. ユーザーのタイムゾーン設定を適用
3. バンドルの状態を`processing`に更新
4. 配信可能条件をチェック（バンドリング設定、未読通知の存在、アカウントアクティブ）
5. 条件を満たす場合、バンドルメールを送信
6. バンドルの状態を`delivered`に更新

**前後の処理との関連**：`DeliverAllJob`がトリガーとなり本ジョブがキューイングされる。バンドル内の通知は、ユーザーの設定期間（bundle_aggregation_period）内に蓄積されたものが対象となる。

**影響範囲**：`notification_bundles`テーブル（状態更新）、メール配信システム

## バッチ種別

通知配信 / メール送信

## 実行スケジュール

| 項目 | 内容 |
|-----|------|
| 実行頻度 | 随時（DeliverAllJobまたは個別トリガー） |
| 実行時刻 | N/A |
| 実行曜日 | N/A |
| 実行日 | N/A |
| トリガー | DeliverAllJob/deliver_later呼び出し |

## 実行条件

### 前提条件

| 条件 | 説明 |
|-----|------|
| バンドルの存在 | 有効な`Notification::Bundle`レコードが存在すること |
| ユーザーの存在 | バンドルに紐づくUserレコードが存在すること |

### 実行可否判定

- `ActiveJob::DeserializationError`が発生した場合はジョブを破棄する
- 配信可能条件（deliverable?）：
  1. ユーザーがバンドリングメール設定をオンにしている
  2. バンドル内に未読通知が存在する
  3. アカウントがアクティブ状態である

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | デフォルト値 | 説明 |
|-------------|-----|-----|-------------|------|
| bundle | Notification::Bundle | Yes | - | 配信対象のバンドルオブジェクト |

### 入力データソース

| データソース | 形式 | 説明 |
|-------------|------|------|
| notification_bundles | DB | 配信対象バンドル情報 |
| users | DB | バンドルに紐づくユーザー情報 |
| notifications | DB | バンドル期間内の通知情報 |
| accounts | DB | ユーザーのアカウント情報 |

## 出力仕様

### 出力データ

| 出力先 | 形式 | 説明 |
|-------|------|------|
| notification_bundles | DB | status更新（processing → delivered） |
| メール配信 | SMTP | バンドルメールの送信 |

### 出力ファイル仕様

ファイル出力なし

## 処理フロー

### 処理シーケンス

```
1. ジョブ開始
   └─ bundle.deliverメソッドを呼び出し
2. タイムゾーン設定
   └─ user.in_time_zone
3. アカウントコンテキスト設定
   └─ Current.with_account(user.account)
4. 状態更新
   └─ processing!
5. 配信可否判定
   └─ deliverable?チェック
6. メール送信（配信可能な場合）
   └─ Notification::BundleMailer.notification(self).deliver
7. 状態更新
   └─ delivered!
8. ジョブ終了
```

### フローチャート

```mermaid
flowchart TD
    A[バッチ開始] --> B[タイムゾーン設定]
    B --> C[アカウントコンテキスト設定]
    C --> D[processing状態に更新]
    D --> E{配信可能?}
    E -->|Yes| F[バンドルメール送信]
    E -->|No| G[スキップ]
    F --> H[delivered状態に更新]
    G --> H
    H --> I[バッチ終了]
```

## データベース操作仕様

### 操作別データベース影響一覧

| 処理 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 更新 | notification_bundles | UPDATE | status更新 |
| 参照 | users | SELECT | ユーザー情報取得 |
| 参照 | notifications | SELECT | 未読通知の確認 |
| 参照 | accounts | SELECT | アカウント状態確認 |

### テーブル別操作詳細

#### notification_bundles

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | status | pending → processing → delivered | 処理状態の遷移 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| DeserializationError | シリアライズエラー | バンドルが削除済み | ジョブを破棄 |
| Net::OpenTimeout | 接続タイムアウト | SMTPサーバーへの接続失敗 | 指数バックオフでリトライ |
| Net::ReadTimeout | 読み取りタイムアウト | SMTPサーバーからの応答待ち | 指数バックオフでリトライ |
| Socket::ResolutionError | DNS解決エラー | SMTPサーバーのDNS解決失敗 | 指数バックオフでリトライ |
| Net::SMTPServerBusy | SMTPサーバービジー | サーバー一時エラー（4xx） | 指数バックオフでリトライ |
| Net::SMTPSyntaxError 501 | 構文エラー | 無効なメールアドレス | ログ出力して破棄 |
| Net::SMTPFatalError 550 | 配信不可 | 不明なユーザー | ログ出力して破棄 |
| Net::SMTPFatalError 552 | サイズ超過 | メッセージサイズ超過 | ログ出力して破棄 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | デフォルト（polynomially_longer） |
| リトライ間隔 | 指数バックオフ（polynomially_longer） |
| リトライ対象エラー | Net::OpenTimeout, Net::ReadTimeout, Socket::ResolutionError, Net::SMTPServerBusy |

### 障害時対応

永続的なエラー（5xx系）の場合はSentryにログを出力し、ジョブを終了する。一時的なエラー（4xx系、タイムアウト）の場合は指数バックオフでリトライする。

## トランザクション仕様

| 項目 | 内容 |
|-----|------|
| トランザクション範囲 | 各状態更新時 |
| コミットタイミング | processing!/delivered!呼び出し時 |
| ロールバック条件 | 例外発生時 |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定処理件数 | 1件/ジョブ実行 |
| 目標処理時間 | メール送信時間に依存 |
| メモリ使用量上限 | メール本文サイズに依存 |

## 排他制御

同時実行可能。ただし、バンドルの状態遷移（pending → processing → delivered）により、同一バンドルが複数回処理されることを防ぐ。

## ログ出力

| ログ種別 | 出力タイミング | 出力内容 |
|---------|--------------|---------|
| 開始ログ | ジョブ開始時 | ActiveJob標準ログ |
| 終了ログ | ジョブ終了時 | ActiveJob標準ログ |
| エラーログ | SMTP永続エラー時 | Sentryに報告（SaaS環境のみ） |

## 監視・アラート

| 監視項目 | 閾値 | アラート先 |
|---------|-----|----------|
| ジョブ失敗率 | 異常な増加 | ジョブ監視システム |
| 配信遅延 | 配信待ちバンドルの滞留 | ジョブ監視システム |

## 備考

- キュー名: `backend`
- `SmtpDeliveryErrorHandling`モジュールをincludeしてSMTPエラー処理を実装
- 配信可能条件（deliverable?）：
  1. `user.settings.bundling_emails?` - ユーザーがバンドリング設定をオン
  2. `notifications.any?` - バンドル期間内に未読通知が存在
  3. `account.active?` - アカウントがアクティブ状態
- `user.in_time_zone`でユーザーのタイムゾーンを適用
- `Current.with_account`でアカウントコンテキストを設定
- メーラー: `Notification::BundleMailer`
- `flush`メソッドで即座に配信可能（ends_atを現在時刻に更新してdeliver_later）
