# 機能設計書 53-お気に入りマージ

## 概要

本ドキュメントは、Shopware Storefrontにおける「お気に入りマージ」機能の設計仕様を定義する。ゲスト状態でローカルストレージに保存されていたお気に入り商品を、ログイン後にサーバー側のウィッシュリストに統合する機能を提供する。

### 本機能の処理概要

**業務上の目的・背景**：ECサイトでは、未ログイン状態（ゲスト）でも商品をお気に入りに追加できることが顧客体験向上に重要である。しかしゲスト状態ではサーバーに保存できないため、ブラウザのローカルストレージに一時保存される。顧客がログインした際に、これらのゲスト時のお気に入りを既存のサーバー側ウィッシュリストに統合（マージ）する必要がある。本機能はその統合処理を担う。

**機能の利用シーン**：ゲスト状態でお気に入りに商品を追加した顧客がログインした際に、フロントエンドJavaScriptが自動的に本機能を呼び出す。これにより、ゲスト時に追加した商品がログイン後のウィッシュリストに自動的にマージされる。

**主要な処理内容**：
1. ログイン状態およびウィッシュリスト機能有効化の確認
2. リクエストから商品IDリストを抽出
3. 商品IDの存在検証（SalesChannelで有効な商品のみ）
4. 顧客の既存ウィッシュリストとの重複チェック
5. 重複を除外した商品をウィッシュリストにupsert
6. イベント発火による拡張ポイント提供
7. マージ完了通知のHTMLレスポンス返却

**関連システム・外部連携**：本機能は Store API (`/store-api/customer/wishlist/merge`) を内部的に利用する。フロントエンドのJavaScriptがローカルストレージからゲストお気に入りを取得し、本APIに送信する。また、`WishlistMergedEvent`イベントを発火し、プラグインによる拡張を可能にする。

**権限による制御**：本機能はログイン必須（`LOGIN_REQUIRED`属性）であり、未認証ユーザーはアクセスできない。また、システム設定`core.cart.wishlistEnabled`が無効の場合はエラーを返却する。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 23 | ウィッシュリストページ | 主画面 | ログイン後のウィッシュリスト表示時にマージ実行 |
| 12 | ログイン/登録ページ | 参照画面 | ログイン成功後にマージ処理がトリガー |

## 機能種別

CRUD操作（Create/Update）/ データ連携 / Ajax API

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| productIds | array/DataBag | Yes | マージする商品IDの配列 | 各IDがUUID形式であること |

### 入力データソース

- リクエストボディ: RequestDataBag形式で商品IDリストを送信
- SalesChannelContext: 顧客情報、販売チャネル情報を含む
- フロントエンド: ブラウザのローカルストレージからゲストお気に入りを取得

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| HTML | string | マージ完了を示すアラートHTML |

### 出力先

- HTTPレスポンス: Twigテンプレートによるアラートメッセージ表示

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信
   └─ WishlistController::ajaxMerge が呼び出される

2. Store API呼び出し
   └─ MergeWishlistProductRoute::merge を実行

3. ウィッシュリスト機能有効性チェック
   └─ core.cart.wishlistEnabled システム設定を確認
   └─ 無効の場合は CustomerException::customerWishlistNotActivated をスロー

4. 商品IDリスト取得
   └─ RequestDataBagから productIds を取得
   └─ 空の場合は処理をスキップ

5. 商品存在チェック
   └─ SalesChannelRepositoryで有効な商品IDのみにフィルタ

6. ウィッシュリストID取得
   └─ 顧客ID + 販売チャネルIDでウィッシュリストを検索
   └─ 存在しない場合は新規UUIDを生成

7. 既存商品との重複チェック
   └─ customer_wishlist_product から既存商品を取得
   └─ 重複商品は既存レコードIDを再利用

8. Upsert実行
   └─ wishlistRepository.upsert でウィッシュリストと商品関連を保存

9. イベント発火
   └─ WishlistMergedEvent をディスパッチ

10. レスポンス返却
    └─ アラートHTMLをレンダリングして返却
```

### フローチャート

```mermaid
flowchart TD
    A[開始: ajaxMerge呼び出し] --> B{ウィッシュリスト機能有効?}
    B -->|No| C[例外: WishlistNotActivated]
    B -->|Yes| D{商品IDリストあり?}
    D -->|No| E[空配列で処理終了]
    D -->|Yes| F[有効な商品IDにフィルタ]
    F --> G[ウィッシュリストID取得/生成]
    G --> H[既存商品との重複チェック]
    H --> I[Upsertデータ構築]
    I --> J[Upsert処理実行]
    J --> K[WishlistMergedEvent発火]
    K --> L[成功アラートHTML返却]
    C --> M[例外キャッチ]
    M --> N[エラーフラッシュメッセージ設定]
    E --> L
    L --> O[終了]
    N --> O
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-53-01 | ログイン必須 | ログイン済み顧客のみが利用可能 | 全リクエスト |
| BR-53-02 | ウィッシュリスト有効化 | システム設定でウィッシュリストが有効である必要がある | 全リクエスト |
| BR-53-03 | 商品存在チェック | SalesChannelで有効な商品のみマージ可能 | 商品マージ時 |
| BR-53-04 | 重複許容 | 既にウィッシュリストにある商品は重複追加されない | 商品マージ時 |
| BR-53-05 | 販売チャネル分離 | ウィッシュリストは販売チャネル毎に分離管理 | 全リクエスト |
| BR-53-06 | 空配列許容 | 商品IDリストが空でもエラーにならない | 商品マージ時 |

### 計算ロジック

特になし

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ウィッシュリスト取得/作成 | customer_wishlist | SELECT/INSERT | 顧客のウィッシュリストを検索、なければ作成 |
| 既存商品取得 | customer_wishlist_product | SELECT | 既にウィッシュリストにある商品を取得 |
| 商品関連追加 | customer_wishlist_product | INSERT | 新規商品をウィッシュリストに追加 |
| 商品存在確認 | product | SELECT | 指定商品IDの存在を確認 |

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

#### customer_wishlist

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id | customer_id = ? AND sales_channel_id = ? | 既存ウィッシュリスト検索 |
| INSERT/UPDATE | id, customer_id, sales_channel_id | 新規または既存のID、顧客ID、販売チャネルID | upsertで処理 |

#### customer_wishlist_product

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, product_id | customer_wishlist_id = ? AND product_id IN (?) | 既存商品取得 |
| INSERT | id, product_id, product_version_id, customer_wishlist_id | 新規UUID、商品ID、LIVE_VERSION、ウィッシュリストID | 新規商品追加 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| CUSTOMER_WISHLIST_NOT_ACTIVATED | 機能無効 | core.cart.wishlistEnabled が false | システム設定を有効化 |
| PRODUCT_IDS_PARAMETER_IS_MISSING | パラメータ不正 | productIdsがDataBag形式でない | 正しい形式でリクエスト |
| - | 認証エラー | 未ログイン状態でアクセス | ログインが必要 |

### リトライ仕様

特になし（同期処理のため）

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

- `wishlistRepository.upsert` 内でトランザクションが管理される
- 複数商品の追加は一つのトランザクション内で実行
- 失敗時は自動ロールバック

## パフォーマンス要件

- レスポンス時間: 1000ms以内（複数商品の処理を含むため）
- Ajax通信のため、非同期処理で画面遷移なし

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

- ログイン必須（`LOGIN_REQUIRED`属性による認証チェック）
- CSRF対策（Symfonyフレームワークによる自動保護）
- 商品IDのUUID形式による不正入力防止
- 他顧客のウィッシュリストへのアクセス不可

## 備考

- ゲストお気に入りはブラウザのローカルストレージに保存される
- マージ後、フロントエンドでローカルストレージをクリアする必要がある
- 無効な商品IDは自動的にスキップされる

---

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

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

### 推奨読解順序

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

まず、ウィッシュリスト商品関連のデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | CustomerWishlistDefinition.php | `src/Core/Checkout/Customer/Aggregate/CustomerWishlist/CustomerWishlistDefinition.php` | ウィッシュリストエンティティ定義 |
| 1-2 | CustomerWishlistProductDefinition.php | `src/Core/Checkout/Customer/Aggregate/CustomerWishlistProduct/CustomerWishlistProductDefinition.php` | 商品関連エンティティ定義 |

**読解のコツ**: マージ処理では既存レコードのIDを再利用することで重複を防いでいる。upsertの動作を理解することが重要。

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

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | WishlistController.php | `src/Storefront/Controller/WishlistController.php` | Storefront側のエントリーポイント。ajaxMergeメソッドがマージの入口 |

**主要処理フロー**:
1. **247-270行目**: ajaxMergeメソッドの定義。RequestDataBag、Request、SalesChannelContext、CustomerEntityを受け取る
2. **259-264行目**: try-catchブロック内でmergeWishlistProductRouteを呼び出し
3. **262-264行目**: 成功時はアラートHTMLをレンダリング
4. **265-267行目**: 例外時はエラーフラッシュメッセージを設定

#### Step 3: Store API ルートを理解する

実際のビジネスロジックはStore APIルートに実装されている。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | MergeWishlistProductRoute.php | `src/Core/Checkout/Customer/SalesChannel/MergeWishlistProductRoute.php` | マージの核心ロジック。重複チェックとupsert処理 |

**主要処理フロー**:
- **62-82行目**: mergeメソッドの実装
- **64-66行目**: ウィッシュリスト機能の有効性チェック
- **68行目**: ウィッシュリストID取得（getWishlistId）
- **70行目**: upsertデータ構築（buildUpsertProducts）
- **72-77行目**: upsert処理でウィッシュリストと商品関連を保存
- **79行目**: WishlistMergedEventイベント発火

#### Step 4: 重複チェックとデータ構築を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | MergeWishlistProductRoute.php | `src/Core/Checkout/Customer/SalesChannel/MergeWishlistProductRoute.php` | buildUpsertProducts、loadCustomerProductsメソッド |

**主要処理フロー**:
- **99-135行目**: buildUpsertProductsメソッド
  - **101-104行目**: RequestDataBagからproductIdsを取得、DataBagでなければ例外
  - **106-110行目**: 空配列なら早期リターン
  - **112行目**: SalesChannelRepositoryで有効な商品IDにフィルタ
  - **114行目**: 既存商品をロード
  - **116-134行目**: 重複チェックしながらupsertデータを構築
- **142-160行目**: loadCustomerProductsメソッド。直接SQLで既存商品を取得

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

```
WishlistController::ajaxMerge (Storefront)
    │
    └─ MergeWishlistProductRoute::merge (Store API)
           │
           ├─ SystemConfigService::get (設定確認)
           │
           ├─ getWishlistId (ウィッシュリストID取得)
           │      └─ EntityRepository::searchIds
           │
           ├─ buildUpsertProducts (upsertデータ構築)
           │      ├─ SalesChannelRepository::searchIds (商品検証)
           │      └─ loadCustomerProducts (既存商品取得)
           │             └─ Connection::createQueryBuilder (直接SQL)
           │
           ├─ EntityRepository::upsert (データ永続化)
           │
           └─ EventDispatcher::dispatch (イベント発火)
                  └─ WishlistMergedEvent
```

### データフロー図

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

productIds (配列) ─────▶ WishlistController::ajaxMerge
                              │
RequestDataBag ────────────▶ │
                              │
SalesChannelContext ───────▶ │
                              │
CustomerEntity ────────────▶ │
                              ▼
                         MergeWishlistProductRoute::merge
                              │
                              ├─▶ product テーブル (SELECT - 存在確認)
                              │
                              ├─▶ customer_wishlist テーブル (SELECT/INSERT)
                              │
                              └─▶ customer_wishlist_product テーブル (SELECT/INSERT)
                                          │
                                          ▼
                                   HTMLレスポンス ──────▶ アラートメッセージ
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| WishlistController.php | `src/Storefront/Controller/WishlistController.php` | ソース | Storefrontコントローラー、エントリーポイント |
| MergeWishlistProductRoute.php | `src/Core/Checkout/Customer/SalesChannel/MergeWishlistProductRoute.php` | ソース | Store APIルート、ビジネスロジック |
| AbstractMergeWishlistProductRoute.php | `src/Core/Checkout/Customer/SalesChannel/AbstractMergeWishlistProductRoute.php` | ソース | 抽象クラス、デコレーションパターン用 |
| CustomerWishlistDefinition.php | `src/Core/Checkout/Customer/Aggregate/CustomerWishlist/CustomerWishlistDefinition.php` | ソース | ウィッシュリストエンティティ定義 |
| CustomerWishlistProductDefinition.php | `src/Core/Checkout/Customer/Aggregate/CustomerWishlistProduct/CustomerWishlistProductDefinition.php` | ソース | ウィッシュリスト商品関連エンティティ定義 |
| WishlistMergedEvent.php | `src/Core/Checkout/Customer/Event/WishlistMergedEvent.php` | ソース | マージイベントクラス |
| alert.html.twig | `src/Storefront/Resources/views/storefront/utilities/alert.html.twig` | テンプレート | アラートメッセージテンプレート |
