# 機能設計書 70-キャンペーントリガー

## 概要

本ドキュメントは、Chatwootにおけるキャンペーンのトリガー機能の設計を記述する。Webウィジェットキャンペーンにおいて、特定の条件（URL、ページ滞在時間等）に基づいてキャンペーンメッセージを表示するトリガー条件を管理する機能を提供する。

### 本機能の処理概要

キャンペーントリガー機能は、Webサイトウィジェット（ongoing）キャンペーンの表示条件を設定・管理する機能を提供する。trigger_rules JSONカラムに条件を格納し、ウィジェット側で条件判定を行ってキャンペーンメッセージを表示する。

**業務上の目的・背景**：ウェブサイト訪問者に対してプロアクティブにメッセージを表示する際、全訪問者に同じメッセージを表示するのではなく、特定のページ（製品ページ、価格ページ、カートページ等）を閲覧している訪問者や、一定時間滞在している訪問者に対して適切なメッセージを表示することで、コンバージョン率の向上が期待できる。

**機能の利用シーン**：
- 製品ページを閲覧中の訪問者に購入サポートを申し出る場合
- 価格ページに30秒以上滞在している訪問者にプロモーションを表示する場合
- 特定のランディングページでウェルカムメッセージを表示する場合
- 営業時間中のみキャンペーンを表示したい場合

**主要な処理内容**：
1. trigger_rules の設定（URL、ページ滞在時間等）
2. trigger_only_during_business_hours フラグの設定
3. ウィジェット側でのトリガー条件判定
4. 条件に合致した場合のキャンペーンメッセージ表示

**関連システム・外部連携**：
- Webウィジェット（JavaScript）：条件判定とメッセージ表示
- インボックスの営業時間設定

**権限による制御**：Campaign policyによるアクセス制御。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 99 | ライブチャットキャンペーン画面 | 主画面 | トリガー条件の設定 |

## 機能種別

CRUD操作（Update）/ バリデーション / フロントエンド判定

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| trigger_rules | Object | No | トリガールール | JSON形式 |
| trigger_rules.url | String | No | 対象URL | http/httpsで始まること |
| trigger_rules.time_on_page | Integer | No | ページ滞在時間（秒） | 数値 |
| trigger_only_during_business_hours | Boolean | No | 営業時間中のみ | default: false |

### 入力データソース

- 画面入力：ライブチャットキャンペーン設定画面
- APIパラメータ

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| trigger_rules | Object | トリガールール |
| trigger_rules.url | String | 対象URL |
| trigger_rules.time_on_page | Integer | ページ滞在時間（秒） |
| trigger_only_during_business_hours | Boolean | 営業時間制限フラグ |

### 出力先

- API レスポンス（JSON形式）
- Webウィジェット（Widget用API経由）

## 処理フロー

### 処理シーケンス

```
1. キャンペーン作成/更新（バックエンド）
   └─ trigger_rules, trigger_only_during_business_hours を保存

2. URLバリデーション
   └─ Website の場合、URL は http/https で始まること

3. ウィジェット読み込み時（フロントエンド）
   └─ Widget API でキャンペーン一覧を取得

4. 条件判定（フロントエンド JavaScript）
   └─ formatCampaigns でデータ整形
   └─ filterCampaigns で条件判定
   └─ URL パターンマッチング
   └─ 営業時間チェック
   └─ ページ滞在時間チェック

5. メッセージ表示
   └─ 条件に合致したキャンペーンのメッセージを表示
```

### フローチャート

```mermaid
flowchart TD
    A[ウィジェット読み込み] --> B[キャンペーン一覧取得]
    B --> C[formatCampaigns]
    C --> D[filterCampaigns]
    D --> E{URL マッチ?}
    E -->|No| F[スキップ]
    E -->|Yes| G{営業時間制限あり?}
    G -->|No| H{滞在時間経過?}
    G -->|Yes| I{営業時間内?}
    I -->|No| F
    I -->|Yes| H
    H -->|No| J[タイマー待機]
    H -->|Yes| K[キャンペーン表示]
    J --> H
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-01 | URL形式検証 | WebsiteキャンペーンのURLはhttp/httpsで始まること | 作成・更新時 |
| BR-02 | URLパターンマッチ | URLPatternポリフィルによるマッチング | ウィジェット判定時 |
| BR-03 | 営業時間制限 | trigger_only_during_business_hours=trueの場合、営業時間外は非表示 | ウィジェット判定時 |
| BR-04 | 滞在時間条件 | time_on_page秒以上滞在した場合に表示 | ウィジェット判定時 |
| BR-05 | ongoing専用 | trigger_rules は ongoing（Website）タイプでのみ有効に機能 | キャンペーン設定時 |

### 計算ロジック

#### URL パターンマッチング（JavaScript）

```javascript
export const isPatternMatchingWithURL = (urlPattern, url) => {
  let updatedUrlPattern = urlPattern;
  const locationObj = new URL(url);

  // 末尾スラッシュの正規化
  if (updatedUrlPattern.endsWith('/')) {
    updatedUrlPattern = updatedUrlPattern.slice(0, -1) + '*\\?*\\#*';
  }
  if (locationObj.pathname.endsWith('/')) {
    locationObj.pathname = locationObj.pathname.slice(0, -1);
  }

  const pattern = new URLPattern(updatedUrlPattern);
  return pattern.test(locationObj.toString());
};
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 作成/更新 | campaigns | INSERT/UPDATE | trigger_rules, trigger_only_during_business_hours 保存 |

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

#### campaigns

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT/UPDATE | trigger_rules | JSON形式 | url, time_on_page 等 |
| INSERT/UPDATE | trigger_only_during_business_hours | Boolean | デフォルト false |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 422 | Unprocessable Entity | URL が http/https で始まらない | 正しいURL形式を入力 |

### リトライ仕様

特にリトライ仕様はなし。

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

- キャンペーン保存は暗黙的なトランザクション管理

## パフォーマンス要件

- ウィジェット側での判定は JavaScript で実行（サーバー負荷なし）
- キャンペーン一覧は Widget API で一括取得

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

- URL パターンはサーバー側でバリデーション
- クライアント側での条件判定のため、機密情報は含めない

## 備考

- URLPattern は Web標準APIで、ポリフィルを使用（urlpattern-polyfill）
- 営業時間設定はインボックスの working_hours から取得

---

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

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

### 推奨読解順序

#### Step 1: モデル定義を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | campaign.rb | `app/models/campaign.rb` | trigger_rules, trigger_only_during_business_hours の定義とバリデーション |

**読解のコツ**: trigger_rules は JSONB カラム、validate_url メソッドでURL形式を検証。

**主要処理フロー**:
- **16行目（スキーマ）**: `trigger_rules :jsonb` - トリガールールのJSONBカラム
- **15行目（スキーマ）**: `trigger_only_during_business_hours :boolean default(FALSE)` - 営業時間制限フラグ
- **39行目**: `validate :validate_url` - URLバリデーション
- **100-104行目**: `validate_url` メソッド
  - trigger_rules['url'] が存在する場合
  - http:// または https:// で始まることを検証

#### Step 2: コントローラーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | campaigns_controller.rb | `app/controllers/api/v1/accounts/campaigns_controller.rb` | trigger_rules パラメータ許可 |

**主要処理フロー**:
- **31-32行目**: `campaign_params` で `:trigger_only_during_business_hours`, `trigger_rules: {}` を許可

#### Step 3: ウィジェット側の判定ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | campaignHelper.js | `app/javascript/widget/helpers/campaignHelper.js` | フロントエンド判定ロジック |

**主要処理フロー**:
- **1行目**: `import { URLPattern } from 'urlpattern-polyfill'` - URLPattern ポリフィル
- **3-17行目**: `isPatternMatchingWithURL` - URLパターンマッチング関数
  - 末尾スラッシュの正規化
  - URLPattern を使用したマッチング
- **20-30行目**: `formatCampaigns` - キャンペーンデータの整形
  - id, triggerOnlyDuringBusinessHours, timeOnPage, url を抽出
- **33-47行目**: `filterCampaigns` - キャンペーンのフィルタリング
  - URL パターンマッチング
  - 営業時間チェック（triggerOnlyDuringBusinessHours && !isInBusinessHours → 非表示）

#### Step 4: JSON出力を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | _campaign.json.jbuilder | `app/views/api/v1/models/_campaign.json.jbuilder` | trigger_rules 出力 |

**主要処理フロー**:
- **20行目**: `json.trigger_rules resource.trigger_rules`
- **21行目**: `json.trigger_only_during_business_hours resource.trigger_only_during_business_hours`

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

```
[バックエンド]

CampaignsController#create/update
    │
    ├─ campaign_params[:trigger_rules] 受信
    ├─ campaign_params[:trigger_only_during_business_hours] 受信
    │
    └─ Campaign#save
           │
           └─ validate :validate_url
                  │
                  └─ trigger_rules['url'] が http/https で始まるか検証

[フロントエンド（ウィジェット）]

Widget 初期化
    │
    ├─ Widget API でキャンペーン一覧取得
    │
    └─ campaignHelper.js
           │
           ├─ formatCampaigns(campaigns)
           │      └─ { id, triggerOnlyDuringBusinessHours, timeOnPage, url } に整形
           │
           └─ filterCampaigns({ campaigns, currentURL, isInBusinessHours })
                  │
                  ├─ isPatternMatchingWithURL(campaign.url, currentURL)
                  │      └─ URLPattern.test() でマッチング
                  │
                  └─ triggerOnlyDuringBusinessHours チェック
                         └─ true && !isInBusinessHours → 除外
```

### データフロー図

```
[バックエンド]

trigger_rules (JSON) ───▶ validate_url ──────────────▶ campaigns テーブル
 (POST body)                   │                        (trigger_rules JSONB)
                               │
                    http/https チェック
                               │
                               ▼
                    JSON レスポンス
                    (trigger_rules, trigger_only_during_business_hours)

[フロントエンド]

Widget API ─────────────▶ formatCampaigns ──────────▶ filterCampaigns
                                │                            │
                         データ整形                    URL マッチ判定
                                │                    営業時間判定
                                │                            │
                                └────────────────────────────┘
                                              │
                                              ▼
                                    キャンペーン表示判定
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| campaign.rb | `app/models/campaign.rb` | モデル | trigger_rules 定義、URLバリデーション |
| campaigns_controller.rb | `app/controllers/api/v1/accounts/campaigns_controller.rb` | コントローラー | trigger_rules パラメータ受信 |
| _campaign.json.jbuilder | `app/views/api/v1/models/_campaign.json.jbuilder` | ビュー | trigger_rules 出力 |
| campaignHelper.js | `app/javascript/widget/helpers/campaignHelper.js` | JavaScript | フロントエンド判定ロジック |
| campaigns_controller.rb | `app/controllers/api/v1/widget/campaigns_controller.rb` | コントローラー | Widget用キャンペーンAPI |
