# 通知設計書 43-Review Form

## 概要

本ドキュメントは、Shopwareにおける商品レビュー投稿時のメール通知仕様を定義する設計書である。顧客がストアフロントから商品レビューを投稿した際に、店舗管理者または指定された受信者に通知メールを送信する機能について記載する。

### 本通知の処理概要

本通知は、ログインした顧客が商品に対してレビュー（評価・タイトル・コメント）を投稿した際に発火するメール通知である。レビューは投稿後すぐには公開されず、管理者の承認が必要となるため、管理者への通知によりレビュー確認・承認ワークフローが開始される。

**業務上の目的・背景**：商品レビューはECサイトにおいて購入判断の重要な参考情報であり、信頼性確保のため管理者による承認制を採用している。レビュー投稿があった際に管理者へ即座に通知することで、迅速なレビュー承認・管理が可能となり、サイトのコンテンツ鮮度を維持できる。

**通知の送信タイミング**：顧客がストアフロントの商品詳細ページでレビューフォームを送信し、バリデーションが成功してレビューがデータベースに保存された直後にReviewFormEventが発火し、Flowビルダー経由でメール送信がトリガーされる。

**通知の受信者**：Flowビルダーの設定により決定される。デフォルトでは管理者向けに送信されるが、レビュー投稿者本人への確認メールとして設定することも可能。

**通知内容の概要**：投稿者名、メールアドレス、対象商品名、評価（5段階）、レビュータイトル、レビュー本文などが含まれる。

**期待されるアクション**：店舗管理者はレビュー内容を確認し、適切であれば承認して公開する。不適切な内容の場合は非公開のままとするか削除する。

## 通知種別

メール通知

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（Flowビルダー経由） |
| 優先度 | 中 |
| リトライ | 有・3回 |

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

1. Flowビルダーの受信者設定（admin/custom/default）に基づく
2. defaultの場合、レビュー投稿者のメールアドレスが使用される
3. adminの場合、管理者ユーザーのメールアドレスが使用される

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | システム設定 `core.basicInformation.email` |
| 送信元名称 | 販売チャネル設定のショップ名 |
| 件名 | New product review |
| 形式 | HTML/テキスト |

### 本文テンプレート

```twig
<div style="font-family:arial; font-size:12px;">
    <p>
        A product review has just been {% if reviewFormData.id is defined %}edited{% else %}sent{% endif %}.<br/>
        <br/>
        {% if reviewFormData.name is defined %}
            Name: {{ reviewFormData.name }}{% if reviewFormData.lastName is defined %} {{ reviewFormData.lastName }}{% endif %}
            <br/>
        {% endif %}
        {% if reviewFormData.email is defined %}
            Email address: {{ reviewFormData.email }}<br/>
            <br>
        {% endif %}
        {% if product.translated.name is defined %}
            Product: {{ product.translated.name }}<br/>
            <br>
        {% endif %}
        Rating: {{ reviewFormData.points }}<br/>
        <br/>
        Title: {{ reviewFormData.title }}<br/>
        <br/>
        Content:<br/>
        {{ reviewFormData.content|nl2br }}<br/>
    </p>
</div>
```

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| なし | - | - | 添付ファイル機能は未対応 |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| reviewFormData | レビューフォーム送信データ | ReviewFormEvent | Yes |
| reviewFormData.id | レビューID（編集時） | product_review.id | No |
| reviewFormData.name | 投稿者名 | フォーム入力/顧客情報 | Yes |
| reviewFormData.lastName | 投稿者姓 | フォーム入力/顧客情報 | No |
| reviewFormData.email | 投稿者メールアドレス | フォーム入力/顧客情報 | Yes |
| reviewFormData.points | 評価（1-5） | フォーム入力 | Yes |
| reviewFormData.title | レビュータイトル | フォーム入力 | Yes |
| reviewFormData.content | レビュー本文 | フォーム入力 | Yes |
| product | 商品エンティティ | product テーブル | Yes |
| product.translated.name | 商品名（翻訳済み） | product_translation | Yes |
| salesChannel | 販売チャネル情報 | sales_channel テーブル | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| API | POST /store-api/product/{productId}/review | バリデーション成功 | Store APIからのリクエスト |
| Flowアクション | review_form.send | Flow条件一致 | ReviewFormEvent発火時 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 未ログイン | ログインが必須（LOGIN_REQUIRED属性） |
| レビュー機能無効 | core.listing.showReviewがfalse |
| バリデーションエラー | 必須フィールド未入力、文字数制限違反 |
| 重複投稿 | 同一顧客・同一商品への再投稿（新規時） |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[顧客がレビュー投稿] --> B{ログイン済み?}
    B -->|No| C[401エラー]
    B -->|Yes| D[ProductReviewSaveRoute::save]
    D --> E{レビュー機能有効?}
    E -->|No| F[ProductException::reviewNotActive]
    E -->|Yes| G[バリデーション実行]
    G -->|エラー| H[ConstraintViolationException]
    G -->|成功| I[レビュー保存 upsert]
    I --> J[ReviewFormEvent生成]
    J --> K[イベントディスパッチ]
    K --> L[Flowビルダー処理]
    L --> M[SendMailAction実行]
    M --> N[メール送信]
    N --> O{送信結果}
    O -->|成功| P[NoContentResponse]
    O -->|失敗| Q[エラーログ記録]
    P --> R[終了]
    Q --> R
    C --> R
    F --> R
    H --> R
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| customer | 顧客情報取得 | ログイン顧客の名前・メールアドレス |
| product | 商品情報取得 | レビュー対象商品 |
| product_review | レビュー重複チェック | 同一顧客・商品の既存レビュー |
| mail_template | メールテンプレート | review_form |
| mail_template_type | テンプレートタイプ | technical_name = 'review_form' |
| system_config | レビュー設定 | core.listing.showReview |

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

#### product_review（重複チェック）

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | レビューID | customerId AND productId |
| customer_id | 顧客ID | 重複チェック |
| product_id | 商品ID | 重複チェック |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| product_review | UPSERT | レビューの新規作成または更新 |

#### product_review

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| UPSERT | product_id | リクエストのproductId | 対象商品 |
| UPSERT | customer_id | ログイン顧客ID | 投稿者 |
| UPSERT | sales_channel_id | 現在の販売チャネルID | 投稿元 |
| UPSERT | language_id | 現在の言語ID | 言語設定 |
| UPSERT | external_user | フォーム入力name | 表示名 |
| UPSERT | external_email | フォーム入力email | 連絡先 |
| UPSERT | title | フォーム入力title | レビュータイトル |
| UPSERT | content | フォーム入力content | レビュー本文 |
| UPSERT | points | フォーム入力points | 評価（1-5） |
| UPSERT | status | false | 承認前は非公開 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 未ログイン | LOGIN_REQUIRED属性チェック | 401 Unauthorized |
| レビュー無効 | showReview設定がfalse | ProductException::reviewNotActive |
| バリデーションエラー | 必須項目未入力、文字数不足 | ConstraintViolationException |
| 重複投稿 | 同一顧客・商品の既存レビュー | EntityNotExists違反 |
| 送信失敗 | SMTPサーバーエラー | エラーログ記録 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 3回 |
| リトライ間隔 | 指数バックオフ |
| リトライ対象エラー | 一時的な送信エラー |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | システム設定に依存 |
| 1日あたり上限 | 制限なし |

### 配信時間帯

制限なし

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

- ログイン必須：LOGIN_REQUIRED属性による認証チェック
- XSS対策：Twigのエスケープ機能による出力サニタイズ
- 重複投稿防止：同一顧客・商品のレビュー重複チェック
- 文字数制限：タイトル最低5文字、本文最低40文字
- メールアドレスバリデーション：EmailIdnConverter使用

## 備考

- MailTemplateTypes::MAILTYPE_REVIEW_FORM 定数で定義
- レビューは投稿直後はstatus=falseで非公開
- 管理者承認後に公開される（status=true）
- Migration1672931011ReviewFormMailTemplateでテンプレート登録
- Migration1672934282ReviewFormSendFlowでFlow設定登録

---

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

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

### 推奨読解順序

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

レビューフォームで使用されるイベントクラスを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | ReviewFormEvent.php | `src/Core/Content/Product/SalesChannel/Review/Event/ReviewFormEvent.php` | イベント名 review_form.send（25行目）、実装インターフェース（23行目） |

**読解のコツ**: ReviewFormEventはMailAware, ProductAware, CustomerAware, FlowEventAwareを実装しており、商品・顧客情報へのアクセスが可能。reviewFormDataプロパティにフォーム送信内容が格納される。

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

API経由でのレビュー投稿処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | ProductReviewSaveRoute.php | `src/Core/Content/Product/SalesChannel/Review/ProductReviewSaveRoute.php` | save()メソッド（64行目〜）がエントリーポイント |

**主要処理フロー**:
1. **67-69行目**: レビュー機能有効チェック（core.listing.showReview）
2. **71-88行目**: 顧客情報取得とデフォルト値設定
3. **91行目**: validate()呼び出しでバリデーション
4. **93-104行目**: レビューデータ構築（status=false）
5. **110行目**: repository.upsert()でDB保存
6. **112-121行目**: ReviewFormEvent生成
7. **123-126行目**: イベントディスパッチ

#### Step 3: バリデーションロジックを理解する

入力値検証の詳細を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | ProductReviewSaveRoute.php | `src/Core/Content/Product/SalesChannel/Review/ProductReviewSaveRoute.php` | validate()メソッド（131行目〜） |

**主要処理フロー**:
- **135行目**: name - NotBlank
- **136行目**: title - NotBlank, Length(min: 5)
- **137行目**: content - NotBlank, Length(min: 40)
- **139行目**: points - GreaterThanOrEqual(1), LessThanOrEqual(5)
- **144-161行目**: 編集時はEntityExists、新規時はEntityNotExists（重複チェック）

#### Step 4: メールテンプレートを理解する

テンプレートファイルの構造を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | en-html.html.twig | `src/Core/Migration/Fixtures/mails/review_form/en-html.html.twig` | テンプレート変数の使用方法、条件分岐 |

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

```
Storefront Review Form
    │
    └─ ProductReviewSaveRoute::save()
           │
           ├─ SystemConfigService::getBool('core.listing.showReview')
           │
           ├─ EmailIdnConverter::encodeDataBag()
           │
           ├─ validate()
           │      ├─ DataValidationDefinition
           │      ├─ NotBlank, Length, GreaterThanOrEqual, LessThanOrEqual
           │      └─ EntityExists / EntityNotExists
           │
           ├─ ProductReviewRepository::upsert()
           │
           └─ EventDispatcher::dispatch(ReviewFormEvent)
                  │
                  └─ Flow Engine
                         │
                         └─ SendMailAction::handleFlow()
                                │
                                └─ MailService::send()
```

### データフロー図

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

POST /store-api/product/           ProductReviewSaveRoute::save()      ReviewFormEvent
  {productId}/review                    │                                    │
  │                                     ├─ 設定チェック                      ├─ review_form.send イベント
  ├─ name                               ├─ バリデーション                    │
  ├─ email                              ├─ レビュー保存                      ▼
  ├─ title                              └─ イベント発火                 Flow Engine
  ├─ content                                                                 │
  └─ points (1-5)                                                            ▼
                                                                        SendMailAction
                                                                             │
                                        ┌────────────────────────────────────┤
                                        ▼                                    ▼
                                  product_review                      Review Form Email
                                  テーブル保存                         (to: 管理者/投稿者)
                                  (status=false)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| ReviewFormEvent.php | `src/Core/Content/Product/SalesChannel/Review/Event/ReviewFormEvent.php` | ソース | レビューフォームイベント |
| ProductReviewSaveRoute.php | `src/Core/Content/Product/SalesChannel/Review/ProductReviewSaveRoute.php` | ソース | Store API エンドポイント |
| MailTemplateTypes.php | `src/Core/Content/MailTemplate/MailTemplateTypes.php` | ソース | MAILTYPE_REVIEW_FORM定数（92行目） |
| en-html.html.twig | `src/Core/Migration/Fixtures/mails/review_form/en-html.html.twig` | テンプレート | 英語HTMLテンプレート |
| en-plain.html.twig | `src/Core/Migration/Fixtures/mails/review_form/en-plain.html.twig` | テンプレート | 英語プレーンテキスト |
| de-html.html.twig | `src/Core/Migration/Fixtures/mails/review_form/de-html.html.twig` | テンプレート | ドイツ語HTMLテンプレート |
| Migration1672931011ReviewFormMailTemplate.php | `src/Core/Migration/V6_5/Migration1672931011ReviewFormMailTemplate.php` | マイグレーション | テンプレート登録 |
| Migration1672934282ReviewFormSendFlow.php | `src/Core/Migration/V6_5/Migration1672934282ReviewFormSendFlow.php` | マイグレーション | Flow設定登録 |
| review-form.html.twig | `src/Storefront/Resources/views/storefront/component/review/review-form.html.twig` | テンプレート | Storefrontフォーム |
