# 機能設計書 79-ユーザーデータエクスポート

## 概要

本ドキュメントは、Fizzyアプリケーションにおける個人ユーザーデータのエクスポート機能の設計を記述する。

### 本機能の処理概要

個人ユーザーがアクセス可能なカードデータをZIPファイルとしてエクスポートする機能である。アカウント全体のエクスポート（No.77）とは異なり、特定のユーザーがアクセス権を持つカードとその関連データ（コメント、添付ファイル等）のみを出力する。

**業務上の目的・背景**：GDPRなどの規制対応として、ユーザーが自身のデータをダウンロードできる「データポータビリティ」の権利を実現する。また、個人的なバックアップや、アカウント退会前のデータ保存にも利用される。アカウント全体ではなく自分に関連するデータのみを取得できるため、プライバシーに配慮した設計となっている。

**機能の利用シーン**：
- GDPR等の規制に基づきユーザーが自身のデータを請求する場合
- アカウントを退会する前に自分のデータを保存したい場合
- 自分が関わったカードの記録を残したい場合

**主要な処理内容**：
1. ユーザーがプロフィール画面からエクスポートをリクエスト
2. バックグラウンドジョブでZIPファイルを生成
3. 完了時にメールでダウンロードリンクを送信
4. ユーザーがリンクからファイルをダウンロード

**関連システム・外部連携**：
- Active Storage：ZIPファイルの保存
- メール送信システム：完了通知メール

**権限による制御**：ユーザーは自分自身のデータのみエクスポート可能。他のユーザーのデータはエクスポートできない。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 47 | データエクスポート画面 | 主画面 | 個人のユーザーデータのエクスポート状況を表示 |

## 機能種別

データ出力（バッチ処理）

## 入力仕様

### 入力パラメータ

#### create（エクスポート開始）
| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| user_id | UUID | Yes | 対象ユーザーのID（URLパラメータ） | Current.userと一致すること |

#### show（ダウンロード表示）
| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| user_id | UUID | Yes | ユーザーID | Current.userと一致すること |
| id | UUID | Yes | エクスポートID | 完了済みのエクスポート |

### 入力データソース

- URLパラメータ：user_id、export id
- セッション：Current.user（実行者・権限確認用）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| ZIPファイル | File | ユーザーがアクセス可能なカードデータを含むZIPアーカイブ |
| 完了通知メール | Email | ダウンロードリンク付きメール |

### 出力先

- Active Storage：ZIPファイル保存
- メール送信：エクスポート完了通知

## 処理フロー

### 処理シーケンス

```
1. ユーザーがプロフィール画面からエクスポートをリクエスト
   └─ createアクションが実行

2. 本人確認
   └─ ensure_current_userで@user == Current.userを確認

3. レート制限確認
   └─ 24時間以内に10件未満であることを確認

4. エクスポートレコード作成
   └─ @user.data_exports.create!(account: Current.account)

5. バックグラウンドジョブをキュー
   └─ export.build_later → DataExportJob.perform_later

6. ジョブ実行
   └─ export.build
   └─ exportable_cardsで対象カード取得
   └─ 各カードをJSONとしてZIPに追加
   └─ 添付ファイルもZIPに追加

7. 完了処理
   └─ mark_completed
   └─ ExportMailer.completed.deliver_later

8. ユーザーがメールのリンクをクリック
   └─ showアクションでダウンロード
```

### フローチャート

```mermaid
flowchart TD
    A[開始: エクスポートリクエスト] --> B{本人確認<br/>@user == Current.user?}
    B -->|No| C[403 Forbidden]
    B -->|Yes| D{レート制限<br/>24h以内に10件未満?}
    D -->|No| E[429 Too Many Requests]
    D -->|Yes| F[エクスポートレコード作成]
    F --> G[DataExportJob<br/>キューイング]
    G --> H[プロフィール画面に<br/>リダイレクト]
    H --> I[バックグラウンド処理]
    I --> J[accessible_cardsで<br/>対象カード取得]
    J --> K[各カードを処理]
    K --> L[JSONとしてZIPに追加]
    L --> M[添付ファイルをZIPに追加]
    M --> N{全カード処理完了?}
    N -->|No| K
    N -->|Yes| O[mark_completed]
    O --> P[完了メール送信]
    P --> Q[終了]
    C --> Q
    E --> Q
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-79-01 | 本人限定 | ユーザーは自分自身のデータのみエクスポート可能 | createアクション時 |
| BR-79-02 | レート制限 | 24時間以内に10件までのエクスポートに制限 | createアクション時 |
| BR-79-03 | アクセス可能カード | ユーザーがアクセス権を持つカードのみエクスポート | ジョブ実行時 |
| BR-79-04 | ファイル有効期限 | ダウンロードリンクは24時間有効 | showアクション時 |
| BR-79-05 | 添付ファイル欠損対応 | 添付ファイルが存在しない場合はスキップ | ジョブ実行時 |

### 計算ロジック

- ファイル名: `fizzy-user-data-export-{export_id}.zip`
- カードファイル名: `{card_number}.json`

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| エクスポート作成 | exports | INSERT | エクスポートレコード作成（type: User::DataExport） |
| ステータス更新 | exports | UPDATE | status更新 |
| カード取得 | cards | SELECT | アクセス可能なカード一覧取得 |
| 件数チェック | exports | SELECT | 24時間以内のエクスポート件数確認 |

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

#### exports

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | account_id | Current.account.id | - |
| INSERT | user_id | @user.id | - |
| INSERT | type | "User::DataExport" | STI |
| INSERT | status | "pending" | デフォルト値 |
| UPDATE | status | "processing" → "completed" | - |
| UPDATE | completed_at | Time.current | 完了時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 403 | 権限エラー | 他ユーザーのエクスポート試行 | Forbiddenエラー |
| 429 | レート制限超過 | 24時間以内に10件以上 | Too Many Requests |
| - | ファイル欠損 | 添付ファイルがストレージに存在しない | スキップして継続 |

### リトライ仕様

バックグラウンドジョブはSolid Queueのデフォルトリトライポリシーに従う。

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

エクスポートレコード作成は単一INSERT。ZIP生成処理はファイル操作のためトランザクション外で実行。

## パフォーマンス要件

- エクスポート開始レスポンス：1秒以内
- ZIP生成時間：カード数と添付ファイルサイズに依存
- includes(:identity)等でN+1クエリを防止

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

- ensure_current_userによる本人確認
- accessible_cardsによるアクセス権チェック
- レート制限による不正リクエスト防止

## 備考

- アカウントエクスポート（No.77）と異なり、個人ユーザー単位のエクスポート
- カードのexport_json、export_attachmentsメソッドを使用
- 添付ファイルが存在しない場合はActiveStorage::FileNotFoundErrorをrescueしてスキップ

---

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

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

### 推奨読解順序

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

ユーザーデータエクスポートのデータ構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | Export | `app/models/export.rb` | 基底クラス（STI）の実装 |
| 1-2 | User::DataExport | `app/models/user/data_export.rb` | ユーザーエクスポート固有の実装 |

**読解のコツ**:
- User::DataExport:
  - 4行目: filenameでfizzy-user-data-export-{id}.zipを定義
  - 7-10行目: populate_zipでカードごとに処理
  - 13-19行目: exportable_cardsでアクセス可能カードを取得（includes付き）
  - 22-31行目: add_card_to_zipでカードとその添付ファイルを追加

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

処理の起点となるコントローラーを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | DataExportsController | `app/controllers/users/data_exports_controller.rb` | create/showアクションの実装 |

**主要処理フロー**:
1. **2行目**: before_action :set_user - ユーザー取得
2. **3行目**: before_action :ensure_current_user - 本人確認
3. **4行目**: before_action :ensure_export_limit_not_exceeded - レート制限
4. **6行目**: CURRENT_EXPORT_LIMIT = 10 - 制限値定義
5. **12-14行目**: createアクション - レコード作成とジョブキューイング
6. **13行目**: @user.data_exports.create!(account: Current.account).build_later
7. **21-23行目**: ensure_current_userの実装

#### Step 3: ビューを理解する

画面表示の実装を確認する（ビューファイルは確認が必要）。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | users/data_exports/show.html.erb | `app/views/users/data_exports/show.html.erb` | ダウンロード画面 |

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

```
Users::DataExportsController#create
    │
    ├─ set_user (before_action)
    │      └─ @user = Current.account.users.find(params[:user_id])
    │
    ├─ ensure_current_user (before_action)
    │      └─ @user == Current.user? ─▶ 403 if false
    │
    ├─ ensure_export_limit_not_exceeded (before_action)
    │      └─ data_exports.current.count >= CURRENT_EXPORT_LIMIT?
    │
    └─ @user.data_exports.create!(account: Current.account).build_later
           │
           ├─ User::DataExport INSERT
           │
           └─ DataExportJob.perform_later(self)
                  │
                  └─ export.build
                         │
                         ├─ processing!
                         │
                         ├─ ZipFile.create_for(file, filename:)
                         │      │
                         │      └─ populate_zip(zip)
                         │             │
                         │             └─ exportable_cards.find_each
                         │                    │
                         │                    └─ add_card_to_zip
                         │                           │
                         │                           ├─ card.export_json
                         │                           │
                         │                           └─ card.export_attachments
                         │
                         ├─ mark_completed
                         │
                         └─ ExportMailer.completed.deliver_later
```

### データフロー図

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

POSTリクエスト ───▶ Users::DataExportsController#create ─┬─▶ リダイレクト
(user_id)               │                                  │   (user profile)
                        │                                  │
                        └─▶ exports INSERT                 └─▶ 通知（開始）

バックグラウンド ───▶ DataExportJob ─────────────────────▶ ZIP生成
                        │                                     │
                        ├─▶ cards SELECT                      │
                        │      (accessible_cards)             │
                        │                                     │
                        └─▶ Export#build                      └─▶ Active Storage
                               │
                               └─▶ populate_zip
                                      │
                                      ├─▶ card.export_json
                                      │
                                      └─▶ card.export_attachments

完了後 ─────────────▶ ExportMailer ───────────────────────▶ 完了メール

メールのリンク ────▶ #show ───────────────────────────────▶ ダウンロード
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| DataExportsController | `app/controllers/users/data_exports_controller.rb` | コントローラー | エクスポート操作のエントリーポイント |
| Export | `app/models/export.rb` | モデル（基底） | エクスポート共通処理 |
| User::DataExport | `app/models/user/data_export.rb` | モデル | ユーザーエクスポート固有処理 |
| DataExportJob | `app/jobs/data_export_job.rb` | ジョブ | バックグラウンド処理 |
| ExportMailer | `app/mailers/export_mailer.rb` | メーラー | 完了通知メール |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義（25行目） |
