# 帳票設計書 2-商品エクスポートCSV

## 概要

本ドキュメントは、Medusaプラットフォームにおける商品エクスポートCSV機能の帳票設計書である。商品データとそのバリアント情報をCSV形式でエクスポートし、商品管理やデータ移行に使用される。

### 本帳票の処理概要

商品エクスポートCSV帳票は、管理者が商品データを一括でCSV形式にエクスポートする機能を提供する。各商品のバリアント単位で1行が出力され、商品情報、バリアント情報、価格情報、オプション情報が含まれる。エクスポート処理はバックグラウンドで非同期に実行される。

**業務上の目的・背景**：ECサイト運営において、商品マスタデータの管理・バックアップ、他システムへのデータ移行、商品情報の一括更新のための元データ取得、および在庫管理システムとの連携が必要となる。本帳票は、商品情報を標準的なCSV形式でエクスポートすることで、これらの業務要件を満たす。特に商品インポート機能との互換性を持ち、エクスポートしたCSVを編集して再インポートすることで商品の一括更新が可能となる。

**帳票の利用シーン**：商品マスタのバックアップ作成時、他のECプラットフォームへのデータ移行時、商品情報の一括編集（価格変更、説明文更新等）の準備時、在庫管理システムとの連携時、商品分析・レポート作成時に利用される。

**主要な出力内容**：
1. 商品基本情報（ID、ハンドル、タイトル、サブタイトル、説明、ステータス、サムネイル等）
2. 商品属性情報（重量、寸法、HSコード、原産国、MIDコード、素材）
3. バリアント情報（ID、タイトル、SKU、バーコード、在庫管理設定等）
4. 価格情報（通貨別価格、リージョン別価格）
5. オプション情報（サイズ、色などのバリアントオプション）
6. 関連情報（画像URL、タグ、カテゴリ、セールスチャネル）

**帳票の出力タイミング**：管理画面の商品一覧から「エクスポート」操作を実行した際に非同期で生成される。生成完了後、通知フィードを通じて管理者にダウンロードリンクが提供される。

**帳票の利用者**：ECサイト管理者、商品管理担当者、マーケティング担当者、在庫管理担当者

## 帳票種別

一覧表（商品データの一覧エクスポート、バリアント単位）

## 利用画面

| 画面No | 画面名 | URL/ルーティング | 出力操作 |
|--------|--------|-----------------|---------|
| 5 | 商品一覧画面 | /admin/products | エクスポートボタン |
| 20 | 商品エクスポート画面 | /admin/products/export | エクスポート実行 |

## 出力形式

### 基本仕様

| 項目 | 内容 |
|-----|------|
| ファイル形式 | CSV |
| 用紙サイズ | 該当なし |
| 向き | 該当なし |
| ファイル名 | `{timestamp}-product-exports.csv`（例：1708785600000-product-exports.csv） |
| 出力方法 | ダウンロード（通知フィード経由） |
| 文字コード | UTF-8 |

### CSV固有設定

| 項目 | 内容 |
|-----|------|
| 区切り文字 | カンマ（,） |
| 囲み文字 | ダブルクォート（"） |
| ヘッダー行 | あり |
| CSVインジェクション対策 | 有効（preventCsvInjection: true） |
| 空フィールド値 | 空文字列 |

## 帳票レイアウト

### レイアウト概要

CSVファイルは1行目にヘッダー、2行目以降にデータ行が出力される。各バリアントが1行として出力され、同じ商品の複数バリアントは商品情報が重複して出力される。

```
┌─────────────────────────────────────────────────────────────┐
│ ヘッダー行（カラム名）                                        │
├─────────────────────────────────────────────────────────────┤
│ データ行1（商品A - バリアント1）                               │
│ データ行2（商品A - バリアント2）                               │
│ データ行3（商品B - バリアント1）                               │
│ ...                                                         │
│ データ行N                                                    │
└─────────────────────────────────────────────────────────────┘
```

### ヘッダー部（CSVカラム定義）

#### 商品基本情報

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | Product Id | 商品の一意識別子 | product.id | 文字列 |
| 2 | Product Handle | URLスラッグ | product.handle | 文字列 |
| 3 | Product Title | 商品タイトル | product.title | 文字列 |
| 4 | Product Subtitle | 商品サブタイトル | product.subtitle | 文字列 |
| 5 | Product Description | 商品説明 | product.description | 文字列 |
| 6 | Product Status | ステータス | product.status | draft/proposed/published/rejected |
| 7 | Product Thumbnail | サムネイルURL | product.thumbnail | URL文字列 |
| 8 | Product External Id | 外部システムID | product.external_id | 文字列 |
| 9 | Product Collection Id | コレクションID | product.collection_id | 文字列 |
| 10 | Product Type Id | 商品タイプID | product.type_id | 文字列 |
| 11 | Product Discountable | 割引適用可否 | product.discountable | true/false |
| 12 | Product Is Giftcard | ギフトカード商品か | product.is_giftcard | true/false |

#### 商品物理属性

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 13 | Product Weight | 重量 | product.weight | 数値 |
| 14 | Product Height | 高さ | product.height | 数値 |
| 15 | Product Width | 幅 | product.width | 数値 |
| 16 | Product Length | 長さ | product.length | 数値 |
| 17 | Product Hs Code | HSコード（関税分類） | product.hs_code | 文字列 |
| 18 | Product Origin Country | 原産国 | product.origin_country | 国コード |
| 19 | Product Mid Code | MIDコード | product.mid_code | 文字列 |
| 20 | Product Material | 素材 | product.material | 文字列 |

#### 商品タイムスタンプ

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 21 | Product Created At | 作成日時 | product.created_at | ISO8601形式 |
| 22 | Product Updated At | 更新日時 | product.updated_at | ISO8601形式 |
| 23 | Product Deleted At | 削除日時 | product.deleted_at | ISO8601形式（null可） |

#### 商品関連情報（動的カラム）

| No | 項目名パターン | 説明 | データ取得元 | 表示形式 |
|----|--------------|------|-------------|---------|
| 24+ | Product Image 1, 2, 3... | 商品画像URL | product.images[n].url | URL文字列 |
| 24+ | Product Tag 1, 2, 3... | タグ値 | product.tags[n].value | 文字列 |
| 24+ | Product Sales Channel 1, 2... | セールスチャネルID | product.sales_channels[n].id | 文字列 |
| 24+ | Product Category 1, 2... | カテゴリID | product.categories[n].id | 文字列 |

#### バリアント基本情報

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| V-1 | Variant Id | バリアントの一意識別子 | variant.id | 文字列 |
| V-2 | Variant Title | バリアントタイトル | variant.title | 文字列 |
| V-3 | Variant Sku | SKU | variant.sku | 文字列 |
| V-4 | Variant Barcode | バーコード | variant.barcode | 文字列 |
| V-5 | Variant Ean | EANコード | variant.ean | 文字列 |
| V-6 | Variant Upc | UPCコード | variant.upc | 文字列 |
| V-7 | Variant Product Id | 所属商品ID | variant.product_id | 文字列 |
| V-8 | Variant Manage Inventory | 在庫管理有無 | variant.manage_inventory | true/false |
| V-9 | Variant Allow Backorder | バックオーダー許可 | variant.allow_backorder | true/false |
| V-10 | Variant Variant Rank | 表示順序 | variant.variant_rank | 数値 |

#### バリアント物理属性

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| V-11 | Variant Weight | 重量 | variant.weight | 数値 |
| V-12 | Variant Height | 高さ | variant.height | 数値 |
| V-13 | Variant Width | 幅 | variant.width | 数値 |
| V-14 | Variant Length | 長さ | variant.length | 数値 |
| V-15 | Variant Hs Code | HSコード | variant.hs_code | 文字列 |
| V-16 | Variant Origin Country | 原産国 | variant.origin_country | 国コード |
| V-17 | Variant Mid Code | MIDコード | variant.mid_code | 文字列 |
| V-18 | Variant Material | 素材 | variant.material | 文字列 |
| V-19 | Variant Metadata | メタデータ | variant.metadata | JSON文字列 |

#### バリアントタイムスタンプ

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| V-20 | Variant Created At | 作成日時 | variant.created_at | ISO8601形式 |
| V-21 | Variant Updated At | 更新日時 | variant.updated_at | ISO8601形式 |
| V-22 | Variant Deleted At | 削除日時 | variant.deleted_at | ISO8601形式（null可） |

#### バリアント価格（動的カラム）

| No | 項目名パターン | 説明 | データ取得元 | 表示形式 |
|----|--------------|------|-------------|---------|
| P-1 | Variant Price {CURRENCY} | 通貨別価格（例：Variant Price USD） | variant.price_set.prices | 数値 |
| P-2 | Variant Price {Region} [{CURRENCY}] | リージョン別価格（例：Variant Price Test Region [USD]） | variant.price_set.prices（price_rules使用） | 数値 |

#### バリアントオプション（動的カラム）

| No | 項目名パターン | 説明 | データ取得元 | 表示形式 |
|----|--------------|------|-------------|---------|
| O-1 | Variant Option 1 Name | オプション1の名前（例：size） | variant.options[0].option.title | 文字列 |
| O-2 | Variant Option 1 Value | オプション1の値（例：large） | variant.options[0].value | 文字列 |
| O-3 | Variant Option 2 Name | オプション2の名前 | variant.options[1].option.title | 文字列 |
| O-4 | Variant Option 2 Value | オプション2の値 | variant.options[1].value | 文字列 |

### 明細部

該当なし（各バリアントが1行として出力される）

### フッター部

該当なし（CSVファイルのためフッターなし）

## 出力条件

### 抽出条件

| 条件名 | 説明 | 必須 |
|-------|------|-----|
| カスタムフィルタ | API呼び出し時に指定されたフィルタ条件（id, collection_id等） | No |
| フィールド選択 | 出力するフィールドの指定（デフォルト：全フィールド） | No |

### ソート順

| 優先度 | 項目 | 昇順/降順 |
|-------|------|---------|
| - | Query Graphのデフォルト順序 | - |
| - | 価格カラム | 通貨コード降順（ZからA） |

### 改ページ条件

該当なし（CSVファイルのため改ページなし）
ただし、データ取得は50件ずつページング処理される（バッチサイズ: 50）

## データベース参照仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 結合条件 |
|-----------|------|---------|
| product | 商品基本情報 | メインエンティティ |
| product_variant | バリアント情報 | product.variants経由 |
| price_set | 価格情報 | variant.price_set経由 |
| product_option | オプション定義 | product.options経由 |
| product_option_value | オプション値 | variant.options経由 |
| image | 商品画像 | product.images経由 |
| product_tag | タグ | product.tags経由 |
| sales_channel | セールスチャネル | product.sales_channels経由 |
| product_category | カテゴリ | product.categories経由 |
| region | リージョン情報 | 価格のリージョンルール用 |

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

#### product

| 参照項目（カラム名） | 帳票項目との対応 | 取得条件 | 備考 |
|-------------------|----------------|---------|------|
| id | Product Id | - | 必須フィールド |
| handle | Product Handle | - | 必須フィールド |
| title | Product Title | - | - |
| subtitle | Product Subtitle | - | - |
| description | Product Description | - | - |
| status | Product Status | - | - |
| thumbnail | Product Thumbnail | - | - |
| external_id | Product External Id | - | - |
| collection_id | Product Collection Id | - | - |
| type_id | Product Type Id | - | - |
| discountable | Product Discountable | - | - |
| is_giftcard | Product Is Giftcard | - | - |
| weight | Product Weight | - | - |
| height | Product Height | - | - |
| width | Product Width | - | - |
| length | Product Length | - | - |
| hs_code | Product Hs Code | - | - |
| origin_country | Product Origin Country | - | - |
| mid_code | Product Mid Code | - | - |
| material | Product Material | - | - |
| created_at | Product Created At | - | - |
| updated_at | Product Updated At | - | - |
| deleted_at | Product Deleted At | - | - |

#### region（価格用）

| 参照項目（カラム名） | 帳票項目との対応 | 取得条件 | 備考 |
|-------------------|----------------|---------|------|
| id | リージョン価格のルックアップ用 | - | - |
| name | リージョン価格カラム名の一部 | - | スペースはアンダースコアに変換 |
| currency_code | リージョン価格カラム名の一部 | - | 大文字で表示 |

## 計算仕様

### 計算項目一覧

| 項目名 | 計算式 | 端数処理 | 備考 |
|-------|-------|---------|------|
| リージョン価格カラム名 | `variant_price_${region.name.toLowerCase().split(" ").join("_")}_[${region.currency_code.toUpperCase()}]` | - | 例：Variant Price Test Region [USD] |
| 通貨価格カラム名 | `variant_price_${currency_code.toUpperCase()}` | - | 例：Variant Price USD |

## 処理フロー

### 出力フロー

```mermaid
flowchart TD
    A[POST /admin/products/export] --> B[exportProductsWorkflow開始]
    B --> C[exportProductsStep実行]
    C --> D{バックグラウンド処理}
    D --> E[リージョン情報取得]
    E --> F[Query Graphで商品データ取得]
    F --> G{データあり?}
    G -->|Yes| H[normalizeForExport]
    H --> I[json2csvでCSV変換]
    I --> J[ファイルストリームに書き込み]
    J --> K{次ページあり?}
    K -->|Yes| F
    K -->|No| L[ファイルアップロード完了]
    G -->|No| L
    L --> M[useQueryGraphStep - ファイル情報取得]
    M --> N[sendNotificationsStep]
    N --> O[通知送信]
    O --> P[完了]
    D -->|失敗時| Q[notifyOnFailureStep]
    Q --> R[エラー通知送信]
```

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 表示メッセージ | 対処方法 |
|----------|---------|--------------|---------|
| エクスポート失敗 | バックグラウンド処理での予期せぬエラー | "Failed to export products, please try again later." | 通知フィードに表示、再試行を促す |
| リージョン未発見 | 価格に設定されたリージョンが見つからない場合 | MedusaError.Types.NOT_FOUND | エラーをスロー、処理中断 |
| データなし | 条件に合致する商品がない場合 | - | 空のCSVファイルが生成される |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定データ件数 | 数百〜数千商品（バリアント数はその数倍） |
| 目標出力時間 | バックグラウンド処理のため制限なし |
| 同時出力数上限 | ワークフローエンジンの設定に依存 |
| バッチサイズ | 50件（デフォルト、カスタマイズ可能） |

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

- CSVインジェクション対策が有効化されている（preventCsvInjection: true）
- Admin API認証が必要（AuthenticatedMedusaRequest）
- 出力ファイルは一時的なストレージに保存され、URLを通じてアクセス
- 商品の機密情報（仕入れ価格等）は含まれないが、運用上の取り扱いには注意が必要

## 備考

- 非同期処理のため、即座にCSVファイルは返却されない
- 処理完了後、通知フィード（admin-ui）を通じてダウンロードリンクが提供される
- MIMEタイプは "text/csv" として設定される
- バリアントのない商品も1行として出力される
- 価格カラムは動的に生成され、存在する通貨・リージョンのみが出力される
- Product Metadata、Product Options、Product Variants、Product Type、Product Collectionは出力から除外される
- Variant Price Set、Variant Optionsは正規化後に除外される
- インポート機能との互換性を持ち、エクスポートしたCSVを編集して再インポートが可能

---

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

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

### 推奨読解順序

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

まず、エクスポートされるデータの構造と型定義を理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | types | `@medusajs/framework/types` | WorkflowTypes.ProductWorkflow.ExportProductsDTOで入力パラメータの型を確認 |
| 1-2 | normalize-for-export.ts | `packages/core/core-flows/src/product/helpers/normalize-for-export.ts` | 正規化後のデータ構造を確認 |

**読解のコツ**: normalizeForExport関数の戻り値の構造を理解することで、CSVの列構成を把握できる。

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

処理の起点となるAPIルートを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | route.ts | `packages/medusa/src/api/admin/products/export/route.ts` | POST関数（行9-21）がエントリーポイント |
| 2-2 | helpers.ts | `packages/medusa/src/api/admin/products/helpers.ts` | remapKeysForProduct関数でフィールド名の変換を確認 |

**主要処理フロー**:
1. **行13**: remapKeysForProductでフィールド名を変換
2. **行14**: フィルタ条件と選択フィールドを入力として設定
3. **行16-18**: exportProductsWorkflowを実行
4. **行20**: トランザクションIDをレスポンスとして返却（202 Accepted）

#### Step 3: ワークフローの構成を理解する

ワークフローの全体像と各ステップの役割を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | export-products.ts | `packages/core/core-flows/src/product/workflows/export-products.ts` | ワークフロー定義（行53-108）、async: true, backgroundExecution: trueの設定（行59-61）に注目 |

**主要処理フロー**:
- **行58-61**: exportProductsStepをバックグラウンドで実行
- **行63-77**: 失敗時の通知設定
- **行79-84**: ファイル情報の取得（useQueryGraphStep）
- **行86-106**: 成功時の通知送信

#### Step 4: CSV出力ステップの詳細を理解する

実際のCSV生成ロジックを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | export-products.ts | `packages/core/core-flows/src/product/steps/export-products.ts` | exportProductsStep（行15-100）でリージョン取得、ページング処理、ストリーム出力を確認 |

**主要処理フロー**:
- **行21-23**: DIコンテナからQuery、FileModule、RegionModuleを取得
- **行25-28**: リージョン情報を取得（価格カラム名生成用）
- **行30-34**: ファイルアップロードストリームを取得
- **行43**: fieldsの定義（id, handle + selectで指定されたフィールド）
- **行45-84**: ページングループでデータ取得・変換・書き込み
- **行60**: normalizeForExportで正規化
- **行62-70**: json2csvでCSV変換（CSVインジェクション対策有効）

#### Step 5: データ正規化ロジックを理解する

商品・バリアントデータの正規化処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | normalize-for-export.ts | `packages/core/core-flows/src/product/helpers/normalize-for-export.ts` | normalizeForExport関数（行10-38）でバリアント単位の行生成を確認 |
| 5-2 | normalize-for-export.ts | 同上 | normalizeProductForExport関数（行40-98）で商品フィールドの正規化を確認 |
| 5-3 | normalize-for-export.ts | 同上 | normalizeVariantForExport関数（行100-160）でバリアント・価格・オプションの正規化を確認 |

**主要処理フロー**:
- **行17-35**: バリアントがある場合、各バリアントに対して商品情報を結合
- **行41-98**: 商品の画像、タグ、セールスチャネル、カテゴリを動的カラムに展開
- **行107-135**: 価格情報をリージョン別・通貨別にカラム化
- **行139-149**: オプション情報を動的カラムに展開

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

```
POST /admin/products/export (route.ts:9)
    │
    ├─ remapKeysForProduct() - フィールド名変換
    │
    └─ exportProductsWorkflow (export-products.ts:53)
           │
           ├─ exportProductsStep (export-products.ts:15) [async, background]
           │      ├─ regionModule.listRegions() - リージョン情報取得
           │      ├─ query.graph() - 商品データ取得
           │      ├─ normalizeForExport() (normalize-for-export.ts:10)
           │      │      ├─ normalizeProductForExport() - 商品正規化
           │      │      │      ├─ prefixFields() - フィールド名にプレフィックス追加
           │      │      │      └─ beautifyKey() - カラム名整形
           │      │      └─ normalizeVariantForExport() - バリアント正規化
           │      │             ├─ 価格カラム生成（通貨別・リージョン別）
           │      │             └─ オプションカラム生成
           │      ├─ json2csv() - CSV変換
           │      └─ writeStream.write() - ファイル出力
           │
           ├─ notifyOnFailureStep - 失敗時通知設定
           │
           ├─ useQueryGraphStep - ファイル情報取得
           │
           └─ sendNotificationsStep - 成功通知送信
```

### データフロー図

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

API Request            exportProductsWorkflow               通知 + CSVファイル
(filter, select)       ┌──────────────────────┐
       │               │                      │
       ▼               │  exportProductsStep  │
┌────────────┐         │  ┌────────────────┐  │              ┌─────────────┐
│ フィルタ   │──────────│▶│ Region取得     │  │              │ CSVファイル  │
│ 条件       │         │  └───────┬────────┘  │              │ (text/csv)  │
└────────────┘         │          │           │              └──────┬──────┘
                       │          ▼           │                     │
                       │  ┌────────────────┐  │                     │
                       │  │ Query Graph    │  │                     │
                       │  └───────┬────────┘  │                     │
                       │          │           │                     │
                       │          ▼           │                     │
                       │  ┌────────────────┐  │                     │
                       │  │ normalize +    │  │                     │
                       │  │ json2csv       │──│─────────────────────┘
                       │  └────────────────┘  │
                       │          │           │
                       │          ▼           │              ┌─────────────┐
                       │  ┌────────────────┐  │              │ 通知         │
                       │  │ File Upload    │  │              │ (admin-ui)  │
                       │  └────────────────┘  │              └──────┬──────┘
                       │                      │                     │
                       │  sendNotifications   │─────────────────────┘
                       └──────────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| route.ts | `packages/medusa/src/api/admin/products/export/route.ts` | ソース | APIエンドポイント定義 |
| export-products.ts | `packages/core/core-flows/src/product/workflows/export-products.ts` | ソース | ワークフロー定義 |
| export-products.ts | `packages/core/core-flows/src/product/steps/export-products.ts` | ソース | CSV出力ステップ |
| normalize-for-export.ts | `packages/core/core-flows/src/product/helpers/normalize-for-export.ts` | ソース | データ正規化ヘルパー |
| middlewares.ts | `packages/medusa/src/api/admin/products/middlewares.ts` | ソース | ルートミドルウェア設定 |
| product.ts | `packages/core/js-sdk/src/admin/product.ts` | ソース | クライアントSDK |
| product-export.spec.ts | `integration-tests/http/__tests__/product/admin/product-export.spec.ts` | テスト | 統合テスト（出力カラムの期待値確認に有用） |
