# 機能設計書 40-商品番号で追加

## 概要

本ドキュメントは、Shopwareストアフロントにおける商品番号（SKU）によるカート追加機能の設計について記述する。

### 本機能の処理概要

本機能は、商品のID（UUID）ではなく商品番号（製品コード/SKU）を入力してカートに商品を追加する機能である。B2Bユーザーやリピート購入者向けに、商品番号を直接入力して素早く商品を追加できるようにする。

**業務上の目的・背景**：B2B向けECサイトや業務用注文システムにおいて、商品番号での直接入力は重要な機能である。カタログや発注書に記載された商品番号をそのまま入力してカートに追加できることで、注文作業の効率化を図る。また、過去の注文履歴から商品番号をコピーして再注文する際にも便利である。

**機能の利用シーン**：
- カートページの商品番号入力フィールドから追加する際
- B2B顧客がカタログの商品番号を入力する際
- 電話注文時にオペレーターが商品番号を入力する際
- 過去の注文から商品番号をコピーして追加する際

**主要な処理内容**：
1. 商品番号（number）パラメータの受け取りとバリデーション
2. 商品番号による商品検索（productNumberで検索）
3. 子商品のみ対象（親バリアント商品は対象外）
4. 商品が見つからない場合のエラーハンドリング
5. LineItemの生成とカートへの追加
6. 成功/エラーメッセージの表示

**関連システム・外部連携**：
- 在庫管理システム（在庫チェック）
- 価格計算エンジン（価格確定）

**権限による制御**：特になし（匿名ユーザーも利用可能）

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 7 | カートページ | 主画面 | 商品番号でカートに商品を追加（CartLineItemController::addProductByNumber） |

## 機能種別

データ検索・作成（Read + Create操作）

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| number | string | Yes | 商品番号（SKU） | 空文字でないこと |

### 入力データソース

- POSTリクエストボディ
- SalesChannelContext（セッション情報、販売チャネル）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| (フラッシュメッセージ) | string | 成功/エラーメッセージ |

### 出力先

- アクションレスポンス（リダイレクト）
- フラッシュメッセージ（セッション）

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信
   └─ CartLineItemController::addProductByNumber()がPOSTリクエストを受信
2. 商品番号パラメータ取得
   └─ request->request->get('number')で取得
3. バリデーション
   └─ 空文字の場合はRoutingExceptionをスロー
4. 検索条件構築
   └─ Criteriaでproduct_numberと一致、かつ子商品のみを条件に設定
5. 商品検索
   └─ ProductListRouteで商品を検索
6. 商品存在チェック
   └─ 結果が空の場合はエラーメッセージを追加してリターン
7. LineItem生成
   └─ ProductLineItemFactoryでLineItemを生成
8. カート取得
   └─ CartServiceで現在のカートを取得
9. カートへの追加
   └─ CartService::add()でカートに追加
10. 結果判定
    └─ エラーなければ成功メッセージを追加
11. アクションレスポンス生成
    └─ createActionResponse()でレスポンス生成
```

### フローチャート

```mermaid
flowchart TD
    A[開始: addProductByNumber POST] --> B[numberパラメータ取得]
    B --> C{number空?}
    C -->|Yes| D[RoutingException: missingRequestParameter]
    C -->|No| E[Criteria構築]
    E --> F[productNumber = number AND childCount条件]
    F --> G[ProductListRoute::load]
    G --> H{商品存在?}
    H -->|No| I[error.productNotFoundメッセージ追加]
    I --> J[アクションレスポンス返却]
    H -->|Yes| K[ProductLineItemFactory::create]
    K --> L[CartService::getCart]
    L --> M[CartService::add]
    M --> N[traceErrors カートエラー処理]
    N --> O{エラーあり?}
    O -->|Yes| P[エラーメッセージ表示]
    O -->|No| Q[成功メッセージ追加]
    P --> R[アクションレスポンス生成]
    Q --> R
    J --> S[終了]
    R --> S
    D --> S
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-40-01 | number必須 | 商品番号パラメータは必須 | 常時 |
| BR-40-02 | 完全一致検索 | 商品番号は完全一致で検索 | 常時 |
| BR-40-03 | 子商品のみ | childCountが0またはnullの商品のみ対象 | 常時 |
| BR-40-04 | 1件のみ取得 | 検索結果は1件に制限 | 常時 |
| BR-40-05 | XSS対策 | エラーメッセージの商品番号はサニタイズ | 常時 |
| BR-40-06 | 数量1 | 追加数量は1固定 | 常時 |

### 計算ロジック

**検索条件**:
```php
$criteria->addFilter(new EqualsFilter('productNumber', $number));
$criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_OR, [
    new EqualsFilter('childCount', 0),
    new EqualsFilter('childCount', null),
]));
```
- 商品番号が完全一致
- AND (childCount = 0 OR childCount = null)
- 親バリアント商品（childCount > 0）は対象外

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 商品検索 | product | SELECT | 商品番号で商品を検索 |
| カート更新 | cart | UPDATE | カートに商品を追加 |

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

#### product

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, product_number, child_count | product_number = {入力値} AND (child_count = 0 OR child_count IS NULL) | LIMIT 1 |

#### cart

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | payload, line_items, price, updated_at | 新しい明細を追加、価格を再計算 | Redis使用時はRedisを更新 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| MISSING_REQUEST_PARAMETER | パラメータ不足 | numberが空の場合 | エラーメッセージ表示 |
| error.productNotFound | 商品未発見 | 商品番号に一致する商品がない場合 | エラーメッセージ表示（商品番号表示） |

### リトライ仕様

特になし（ユーザー操作による再試行）

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

- 商品検索は読み取り専用
- カート更新は単一トランザクションで実行

## パフォーマンス要件

- レスポンス時間: 500ms以内
- 商品番号検索は1件に制限して効率化
- Profiler::traceによるパフォーマンス計測対応

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

- セッショントークンによるカート分離
- XSS対策としてHtmlSanitizerで商品番号をサニタイズ
- SQLインジェクション対策としてDAL使用
- 商品番号の表示時にエスケープ処理

## 備考

- Profiler::trace('cart::add-product-by-number')でパフォーマンス計測
- 親バリアント商品を除外することで、購入可能な商品のみを対象

---

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

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

### 推奨読解順序

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

まず、商品番号検索で使用されるデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | ProductEntity.php | `src/Core/Content/Product/ProductEntity.php` | 商品エンティティ（productNumber, childCount） |
| 1-2 | Criteria.php | `src/Core/Framework/DataAbstractionLayer/Search/Criteria.php` | 検索条件の構造 |

**読解のコツ**: productNumberはユニークな商品識別子。childCountは子バリアントの数で、0またはnullの場合は購入可能な商品を示す。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | CartLineItemController.php | `src/Storefront/Controller/CartLineItemController.php` | addProductByNumberメソッドがエントリーポイント |

**主要処理フロー**:
1. **219行目**: ルート定義（/checkout/product/add-by-number）
2. **220行目**: addProductByNumberメソッドの開始
3. **221-227行目**: Profiler::trace内でnumber取得とバリデーション
4. **229-235行目**: Criteria構築（productNumber一致 + childCount条件）
5. **237行目**: ProductListRoute::loadで商品検索
6. **239-246行目**: 商品未発見時のエラーハンドリング
7. **248行目**: productIdの取得
8. **250行目**: ProductLineItemFactory::createでLineItem生成
9. **252行目**: CartService::getCartでカート取得
10. **254行目**: CartService::addでカート追加
11. **256-258行目**: 成功メッセージ追加
12. **260行目**: createActionResponseでレスポンス生成

#### Step 3: 検索条件を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | CartLineItemController.php | `src/Storefront/Controller/CartLineItemController.php` | 検索条件の構築部分 |

**主要処理フロー**:
- **229-230行目**: Criteriaの初期化とLIMIT設定
- **231行目**: productNumber完全一致フィルター
- **232-235行目**: childCount条件（OR条件で0またはnull）

**読解のコツ**: MultiFilter::CONNECTION_ORで「childCount=0 OR childCount=null」のいずれかにマッチする条件を設定。

#### Step 4: サニタイズ処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | HtmlSanitizer.php | `src/Core/Framework/Util/HtmlSanitizer.php` | HTMLサニタイズ処理 |

**読解のコツ**: エラーメッセージに商品番号を含める際、XSS対策としてHtmlSanitizerでサニタイズしている（242行目）。

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

```
CartLineItemController::addProductByNumber()
    │
    ├─ Profiler::trace('cart::add-product-by-number')
    │
    ├─ request->request->get('number') [パラメータ取得]
    │
    ├─ Criteria構築
    │      ├─ setLimit(1)
    │      ├─ addFilter(EqualsFilter('productNumber', $number))
    │      └─ addFilter(MultiFilter(OR, [childCount=0, childCount=null]))
    │
    ├─ ProductListRoute::load() [商品検索]
    │      └─ ProductCollection
    │
    ├─ (商品なし) → HtmlSanitizer::sanitize() → エラーメッセージ
    │
    ├─ ProductLineItemFactory::create() [LineItem生成]
    │      └─ LineItem
    │
    ├─ CartService::getCart() [カート取得]
    │
    ├─ CartService::add() [カート追加]
    │      │
    │      └─ AbstractCartItemAddRoute::add()
    │
    ├─ traceErrors() [エラー処理]
    │
    └─ createActionResponse() [レスポンス生成]
```

### データフロー図

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

number ──────────────▶ Criteria構築 ─────────────▶ ProductListRoute
   │                         │                           │
   │                         ▼                           ▼
   │                 productNumber =              product テーブル
   │                 AND childCount                       │
   │                                                      ▼
   │                                              ProductCollection
   │                                                      │
   │                    ┌─────────────────────────────────┤
   │                    ▼                                 ▼
   │              結果なし                          結果あり
   │                    │                                 │
   │                    ▼                                 ▼
   │         HtmlSanitizer::sanitize      ProductLineItemFactory::create
   │                    │                                 │
   │                    ▼                                 ▼
   │         error.productNotFound              CartService::add
   │              メッセージ                              │
   │                    │                                 ▼
   │                    │                         cart テーブル
   │                    │                           (Redis)
   │                    │                                 │
   │                    │                                 ▼
   │                    │                    checkout.addToCartSuccess
   │                    │                              メッセージ
   │                    │                                 │
   └────────────────────┼─────────────────────────────────┤
                        │                                 │
                        ▼                                 ▼
                              ActionResponse
                           (リダイレクト)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| CartLineItemController.php | `src/Storefront/Controller/CartLineItemController.php` | コントローラー | HTTPリクエスト処理 |
| AbstractProductListRoute.php | `src/Core/Content/Product/SalesChannel/AbstractProductListRoute.php` | 抽象クラス | 商品検索ルート契約 |
| ProductListRoute.php | `src/Core/Content/Product/SalesChannel/ProductListRoute.php` | Store-APIルート | 商品検索処理 |
| ProductLineItemFactory.php | `src/Core/Checkout/Cart/LineItemFactoryHandler/ProductLineItemFactory.php` | ファクトリ | 商品LineItem生成 |
| CartService.php | `src/Core/Checkout/Cart/SalesChannel/CartService.php` | サービス | カート操作 |
| HtmlSanitizer.php | `src/Core/Framework/Util/HtmlSanitizer.php` | ユーティリティ | HTMLサニタイズ |
| Criteria.php | `src/Core/Framework/DataAbstractionLayer/Search/Criteria.php` | 検索条件 | クエリ条件構築 |
| EqualsFilter.php | `src/Core/Framework/DataAbstractionLayer/Search/Filter/EqualsFilter.php` | フィルター | 完全一致条件 |
| MultiFilter.php | `src/Core/Framework/DataAbstractionLayer/Search/Filter/MultiFilter.php` | フィルター | OR/AND条件 |
