# 通知設計書 13-インポート失敗メール

## 概要

本ドキュメントは、Fizzyアプリケーションにおけるインポート失敗メール通知の設計仕様を定義する。データインポート処理が失敗した際に、インポートを要求したユーザーに対して失敗理由を含む通知メールを送信する機能について記載する。

### 本通知の処理概要

本通知は、ユーザーがアップロードしたエクスポートZIPファイルからのデータインポート処理がバックグラウンドジョブで失敗した際に自動送信されるメール通知である。

**業務上の目的・背景**：データインポートは複雑な処理であり、様々な理由で失敗する可能性がある。ユーザーがインポート失敗を認識し、適切な対処（再試行、サポートへの問い合わせなど）を行えるようにするため、失敗通知が必要となる。失敗理由を明確に伝えることで、ユーザーの自己解決を促進し、サポート負荷を軽減する効果もある。

**通知の送信タイミング**：Account::Importモデルのprocess/checkメソッド内でエラーが発生し、mark_as_failedが呼ばれたタイミングで送信される。具体的には、ConflictError、IntegrityError、InvalidFileError、またはその他の例外が捕捉された際に、ImportMailer.failed(self).deliver_laterが実行される。

**通知の受信者**：インポートを要求したユーザーのIdentityに紐づくメールアドレスに送信される。送信先は`import.identity.email_address`で決定される。

**通知内容の概要**：メール本文には、インポートが失敗した旨のメッセージと、失敗理由に応じた詳細説明が含まれる。失敗理由は3パターンあり、(1)既存アカウントとの競合（conflict）、(2)無効なエクスポートファイル（invalid_export）、(3)その他（破損データ等）である。

**期待されるアクション**：ユーザーは失敗理由を確認し、適切な対処を行う。競合の場合は別のアカウント名での再試行、無効なファイルの場合は正しいエクスポートファイルの取得、その他の場合は新しいエクスポートでの再試行またはサポートへの問い合わせが期待される。

## 通知種別

メール通知

## 送信仕様

### 基本情報

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

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

インポートを要求したユーザーのIdentityに紐づくメールアドレスに送信される。送信先は`import.identity.email_address`で決定される。

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | 環境変数MAILER_FROM_ADDRESSまたはデフォルト値 |
| 送信元名称 | Fizzy |
| 件名 | Your Fizzy account import failed |
| 形式 | HTML/テキスト（マルチパート） |

### 本文テンプレート

#### HTML版

```html
<p class="subtitle">Unfortunately, we couldn't import your Fizzy account.</p>

<!-- 失敗理由に応じた詳細メッセージ -->
<!-- conflict の場合 -->
<p>It looks like the account you are trying to import already exists.</p>

<!-- invalid_export の場合 -->
<p>The ZIP file isn't a Fizzy account export.</p>

<!-- その他の場合 -->
<p>This may be due to corrupted export data or a conflict with existing data. Please try again with a fresh export, or reach out for help if the problem persists.</p>

<p class="footer">Need help? <a href="mailto:support@fizzy.do">Send us an email</a>.</p>
```

#### テキスト版

```
Unfortunately, we couldn't import your Fizzy account.

<!-- 失敗理由に応じた詳細メッセージ -->
<!-- conflict の場合 -->
It looks like the account you are trying to import already exists.

<!-- invalid_export の場合 -->
The ZIP file isn't a Fizzy account export.

<!-- その他の場合 -->
This may be due to corrupted export data or a conflict with existing data. Please try again with a fresh export, or reach out for help if the problem persists.

Need help? Send us an email at support@fizzy.do
```

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| @import | インポートオブジェクト | 引数import | Yes |
| @import.failed_due_to_conflict? | 競合エラー判定 | account_imports.failure_reason | Yes |
| @import.failed_due_to_invalid_export? | 無効エクスポート判定 | account_imports.failure_reason | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| バッチ | Account::DataImportJobの失敗 | インポート処理が異常終了 | Account::Import#mark_as_failed内で送信 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| インポート処理成功 | statusがcompletedの場合は完了通知が送信される |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[Account::DataImportJob実行] --> B{check処理}
    B -->|ConflictError| C[mark_as_failed :conflict]
    B -->|IntegrityError/InvalidFileError| D[mark_as_failed :invalid_export]
    B -->|その他のエラー| E[mark_as_failed nil]
    B -->|成功| F{process処理}
    F -->|ConflictError| C
    F -->|IntegrityError/InvalidFileError| D
    F -->|その他のエラー| E
    F -->|成功| G[mark_completed]
    C --> H[ImportMailer.failed.deliver_later]
    D --> H
    E --> H
    H --> I[メール送信キューに追加]
    I --> J[メール送信実行]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| account_imports | インポート情報取得 | ステータス、失敗理由取得 |
| identities | メールアドレス取得 | 送信先決定 |

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

#### account_imports

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | インポート識別 | 引数で渡される |
| identity_id | 送信先Identity特定 | belongs_to関連 |
| status | 状態確認 | failed時のみ送信 |
| failure_reason | 失敗理由判定 | メール本文分岐に使用 |

#### identities

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| email_address | 送信先メールアドレス | import.identity |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| account_imports | UPDATE | status, failure_reason更新 |

#### account_imports更新

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| UPDATE | status | failed | 失敗ステータス |
| UPDATE | failure_reason | conflict / invalid_export / nil | 失敗理由 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | SMTPエラー等 | Active Jobによるリトライ |
| 宛先不正 | メールアドレス形式不正 | 送信エラーとしてログ記録 |

### リトライ仕様

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

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 特に制限なし |
| 1日あたり上限 | 特に制限なし |

### 配信時間帯

特に制限なし（インポート失敗時に即座に送信）

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

- 失敗理由は一般的なメッセージのみを表示し、技術的な詳細（スタックトレース等）は含めない
- 失敗したインポートは7日後に自動削除される（Account::Import.cleanup）
- 失敗したインポートに関連するアカウントも自動削除される（Account::Import#cleanup）
- メールアドレスはIdentityテーブルで管理され、認証済みのアドレスのみ使用

## 備考

- 失敗理由は3パターン：conflict（既存アカウントとの競合）、invalid_export（無効なエクスポートファイル）、nil（その他のエラー）
- IntegrityErrorとInvalidFileErrorは同じinvalid_export理由として扱われる
- Account::DataImportJobではIntegrityErrorとInvalidFileErrorはdiscard_onで処理され、ジョブとしてはリトライされない
- 失敗したアカウントとインポートレコードはcleanupメソッドで7日後に削除される

---

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

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

### 推奨読解順序

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

まず、インポート機能のエラーハンドリングとステータス管理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | account/import.rb | `app/models/account/import.rb` | enum定義（行9-10）でstatus/failure_reasonの値を確認。mark_as_failedメソッド（行77-79）がトリガー |

**読解のコツ**: rescueブロック（行30-38、55-63）でエラー種別ごとの分岐を理解する。ConflictError→conflict、IntegrityError/InvalidFileError→invalid_export、その他→nilとして記録される。

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

失敗時のジョブ処理フローを確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | account/data_import_job.rb | `app/jobs/account/data_import_job.rb` | discard_on設定（行5）でIntegrityError/InvalidFileErrorはジョブリトライなし |

**主要処理フロー**:
- **行5**: `discard_on` - 特定のエラーではジョブをリトライせず破棄

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

メール送信処理の実装を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | import_mailer.rb | `app/mailers/import_mailer.rb` | failedメソッド（行7-10）でimportオブジェクトを受け取る |

**主要処理フロー**:
- **行7-10（import_mailer.rb）**: failedメソッドでimportを設定し、メール送信

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

失敗理由に応じた条件分岐を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | failed.html.erb | `app/views/mailers/import_mailer/failed.html.erb` | 条件分岐による失敗理由表示 |
| 4-2 | failed.text.erb | `app/views/mailers/import_mailer/failed.text.erb` | テキスト版の条件分岐 |

**主要処理フロー**:
- **行3-9（failed.html.erb）**: `failed_due_to_conflict?`、`failed_due_to_invalid_export?`による分岐

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

```
Account::DataImportJob#perform
    │
    ├─ step :check
    │      └─ import.check
    │             └─ rescue → mark_as_failed(reason)
    │
    └─ step :process
           └─ import.process
                  └─ rescue → mark_as_failed(reason)
                         │
                         ├─ ConflictError → :conflict
                         ├─ IntegrityError → :invalid_export
                         ├─ InvalidFileError → :invalid_export
                         └─ その他 → nil
                                │
                                └─ ImportMailer.failed(self).deliver_later
                                       └─ [非同期] メール送信
```

### データフロー図

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

ZIPファイル ───▶ Account::Import(processing)
                       │
                       ▼
               check/process処理
                       │
                       ▼
               エラー発生
                       │
                       ▼
               mark_as_failed(reason) ───▶ Account::Import(failed)
                       │
                       ▼
               ImportMailer.failed
                       │
                       ├─▶ conflict? → "account already exists"
                       ├─▶ invalid_export? → "isn't a Fizzy export"
                       └─▶ その他 → "corrupted export data"
                               │
                               ▼
                       失敗メール ───▶ ユーザーのメールボックス
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| account/import.rb | `app/models/account/import.rb` | ソース | インポートモデル、エラーハンドリング |
| account/data_import_job.rb | `app/jobs/account/data_import_job.rb` | ソース | インポートジョブ |
| import_mailer.rb | `app/mailers/import_mailer.rb` | ソース | インポートメーラー |
| application_mailer.rb | `app/mailers/application_mailer.rb` | ソース | 基底メーラー |
| failed.html.erb | `app/views/mailers/import_mailer/failed.html.erb` | テンプレート | HTML版メール本文 |
| failed.text.erb | `app/views/mailers/import_mailer/failed.text.erb` | テンプレート | テキスト版メール本文 |
| import_mailer_test.rb | `test/mailers/import_mailer_test.rb` | テスト | メーラーテスト |
