# 機能設計書 211-Liquidテンプレート

## 概要

本ドキュメントは、Chatwootにおける「Liquidテンプレート」機能の設計仕様を記載する。本機能は、Liquidテンプレートエンジンを使用して動的なメッセージコンテンツを生成するためのサービスを提供する。

### 本機能の処理概要

この機能は、メッセージやキャンペーンのコンテンツ内でLiquidテンプレート構文を解析・展開し、連絡先、エージェント、会話、インボックス、アカウントの情報を動的に埋め込む機能を提供する。

**業務上の目的・背景**：顧客とのコミュニケーションにおいて、パーソナライズされたメッセージは顧客体験の向上に大きく寄与する。一括送信されるキャンペーンメッセージや自動返信においても、連絡先の名前やカスタム属性を動的に挿入することで、一対一の対話のような親近感のあるコミュニケーションを実現する。また、テンプレート管理の効率化により、エージェントの作業負荷を軽減し、一貫性のあるメッセージングを可能にする。

**機能の利用シーン**：
- キャンペーンメッセージで連絡先の名前や属性を自動挿入する場合
- 送信メッセージで動的なプレースホルダを展開する場合
- WhatsApp等のテンプレートメッセージで変数パラメータを処理する場合
- メールテンプレートでLiquid構文を使用する場合

**主要な処理内容**：
1. メッセージコンテンツ内のLiquid構文を検出し、パース処理を実行
2. コードブロック（バッククォート内）はLiquid処理から除外
3. ContactDrop、UserDrop、InboxDrop、AccountDrop、ConversationDrop等のDropオブジェクトを生成
4. Liquid::Templateでテンプレートをレンダリングし、変数をDropオブジェクトの値で置換
5. テンプレートパラメータ（WhatsApp等）内のLiquid変数も再帰的に処理

**関連システム・外部連携**：
- Liquid gem（Shopifyが開発したテンプレートエンジン）
- ActionView::Template::Handlers（Rails用Liquidテンプレートハンドラ）

**権限による制御**：本機能自体に権限制御はないが、キャンペーン作成やメッセージ送信の権限がある場合にのみ利用可能となる。

## 関連画面

本機能はバックエンド処理であり、直接関連する画面はない。ただし、以下の画面からの操作時に間接的に呼び出される。

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 99 | ライブチャットキャンペーン画面 | 参照画面 | キャンペーンメッセージでLiquid変数を使用 |
| 100 | SMSキャンペーン画面 | 参照画面 | SMSキャンペーンでLiquid変数を使用 |
| 101 | WhatsAppキャンペーン画面 | 参照画面 | WhatsAppキャンペーンでLiquid変数を使用 |
| 11 | 会話詳細画面 | 参照画面 | メッセージ送信時にLiquid変数を展開 |

## 機能種別

テンプレート処理 / データ変換

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| campaign | Campaign | Yes | キャンペーンオブジェクト | 存在チェック |
| contact | Contact | Yes | 連絡先オブジェクト | 存在チェック |
| message | String | Yes | Liquid構文を含むメッセージ本文 | なし |
| conversation | Conversation | Context | 会話オブジェクト（Liquidable利用時） | コンテキスト依存 |
| sender | User | Context | 送信者オブジェクト（Liquidable利用時） | コンテキスト依存 |

### 入力データソース

- Campaign: campaigns テーブル
- Contact: contacts テーブル
- Conversation: conversations テーブル
- Message: messages テーブル（content, additional_attributes）
- User: users テーブル

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| rendered_message | String | Liquid変数が展開されたメッセージ本文 |

### 出力先

- メッセージのcontentカラム（Liquidable concern使用時）
- メッセージのadditional_attributes['template_params']['processed_params']（テンプレートパラメータ使用時）
- キャンペーン送信時のメッセージ本文

## 処理フロー

### 処理シーケンス

```
1. メッセージ/キャンペーン作成リクエスト受信
   └─ Liquid構文を含むコンテンツが入力される

2. コードブロック保護処理
   └─ バッククォート内のコンテンツを{% raw %}タグで保護

3. Dropオブジェクト生成
   └─ ContactDrop, UserDrop, InboxDrop, AccountDrop, ConversationDrop等を生成

4. Liquidテンプレートパース
   └─ Liquid::Template.parseでテンプレートを解析

5. テンプレートレンダリング
   └─ template.render(drops)でDropオブジェクトの値を展開

6. エラーハンドリング
   └─ Liquid::Errorが発生した場合は元のメッセージを返却

7. 結果返却/保存
   └─ 展開されたコンテンツを返却または保存
```

### フローチャート

```mermaid
flowchart TD
    A[メッセージ作成開始] --> B{Liquid処理対象?}
    B -->|No| Z[元のメッセージを返却]
    B -->|Yes| C[コードブロック保護]
    C --> D[Dropオブジェクト生成]
    D --> E[Liquid::Template.parse]
    E --> F{パース成功?}
    F -->|No| Z
    F -->|Yes| G[template.render]
    G --> H{レンダリング成功?}
    H -->|No| Z
    H -->|Yes| I[展開されたコンテンツを返却]
    Z --> J[終了]
    I --> J
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-211-01 | コードブロック保護 | バッククォート内のコンテンツはLiquid処理から除外する | メッセージにバッククォートが含まれる場合 |
| BR-211-02 | エラー時フォールバック | Liquid構文エラー時は元のメッセージをそのまま使用する | Liquid::Error発生時 |
| BR-211-03 | 対象メッセージタイプ | outgoingまたはtemplateタイプのメッセージのみ処理対象 | Liquidable concern使用時 |
| BR-211-04 | 名前の正規化 | 連絡先・エージェント名は各単語の先頭を大文字化する | Dropオブジェクトのname/first_name/last_name取得時 |

### 計算ロジック

名前の正規化処理:
```ruby
name.split.map(&:capitalize).join(' ')
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| Liquid処理 | messages | UPDATE（before_create） | contentカラムを展開後の値で更新 |
| テンプレートパラメータ処理 | messages | UPDATE（before_create） | additional_attributesを更新 |

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

#### messages

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | content | Liquid展開後のメッセージ本文 | before_createコールバック |
| UPDATE | additional_attributes | 展開後のtemplate_params | テンプレートメッセージ時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | Liquid::SyntaxError | 不正なLiquid構文 | 元のメッセージをそのまま使用 |
| - | Liquid::Error | その他のLiquidエラー | 元のメッセージをそのまま使用 |
| - | NoMethodError | Dropオブジェクトで未定義メソッド呼び出し | try()で安全にアクセス |

### リトライ仕様

本機能にリトライ処理は存在しない。エラー発生時は即座にフォールバック処理を実行する。

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

本機能単体でのトランザクション管理はない。メッセージ作成のbefore_createコールバック内で実行されるため、親トランザクションに依存する。

## パフォーマンス要件

- Liquidテンプレートのパース・レンダリングは同期処理で実行
- 大量のDropオブジェクト生成時のメモリ使用量に注意
- テンプレートパラメータの再帰処理は深さに上限なし

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

- Liquidは設計上安全なテンプレート言語であり、任意のコード実行を防止
- Dropオブジェクトは公開して安全な属性のみを公開
- 連絡先のカスタム属性はcustom_attributeメソッド経由でアクセス

## 備考

- LiquidはShopifyが開発したテンプレートエンジン
- コードブロック保護は正規表現 `/`(.*?)`/m` で実装
- ビューテンプレート用にActionView::Template::Handlers::Liquidも利用可能

---

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

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

### 推奨読解順序

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

まず、Liquidテンプレートで使用されるDropオブジェクトの構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | base_drop.rb | `app/drops/base_drop.rb` | 全Dropクラスの基底クラス。id, nameメソッドの基本実装 |
| 1-2 | contact_drop.rb | `app/drops/contact_drop.rb` | 連絡先情報のDrop。name, email, phone_number, first_name, last_name, custom_attribute |
| 1-3 | user_drop.rb | `app/drops/user_drop.rb` | エージェント情報のDrop。name, available_name, first_name, last_name |
| 1-4 | inbox_drop.rb | `app/drops/inbox_drop.rb` | インボックス情報のDrop。name |
| 1-5 | account_drop.rb | `app/drops/account_drop.rb` | アカウント情報のDrop。name |
| 1-6 | conversation_drop.rb | `app/drops/conversation_drop.rb` | 会話情報のDrop。display_id, contact_name, recent_messages, custom_attribute |
| 1-7 | message_drop.rb | `app/drops/message_drop.rb` | メッセージ情報のDrop。sender_display_name, text_content |

**読解のコツ**:
- 全てのDropクラスは`Liquid::Drop`を継承した`BaseDrop`から派生
- `try()`メソッドでnilセーフなアクセスを実現
- カスタム属性は`transform_keys(&:to_s)`で文字列キーに変換

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

キャンペーン用のLiquidテンプレートサービスと、メッセージ用のLiquidable concernを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | campaign_template_service.rb | `app/services/liquid/campaign_template_service.rb` | キャンペーン用のLiquidテンプレート処理サービス |
| 2-2 | liquidable.rb | `app/models/concerns/liquidable.rb` | メッセージモデルにincludeされるLiquid処理concern |

**主要処理フロー**:

**campaign_template_service.rb**:
1. **2-4行目**: `pattr_initialize [:campaign!, :contact!]` - campaign, contactを必須パラメータとして初期化
2. **4-6行目**: `call(message)` - メインエントリーポイント
3. **10-17行目**: `message_drops` - Dropオブジェクトのハッシュを生成
4. **19-25行目**: `process_liquid_in_content` - コードブロック保護、パース、レンダリング

**liquidable.rb**:
1. **5-6行目**: `before_create`コールバックでLiquid処理を自動実行
2. **11-19行目**: `message_drops` - メッセージコンテキスト用のDropオブジェクト生成
3. **21-23行目**: `liquid_processable_message?` - 処理対象判定（outgoing or template）
4. **25-32行目**: `process_liquid_in_content` - メッセージ本文のLiquid処理
5. **40-51行目**: `process_liquid_in_template_params` - テンプレートパラメータのLiquid処理
6. **65-95行目**: Hash/Array/Stringの再帰的Liquid処理

#### Step 3: Rails統合を理解する

Railsのビューテンプレートとして.liquidファイルを使用するためのハンドラ。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | liquid_handler.rb | `config/initializers/liquid_handler.rb` | Liquidテンプレートハンドラの登録 |
| 3-2 | liquid.rb | `lib/action_view/template/handlers/liquid.rb` | ActionView用Liquidテンプレートハンドラの実装 |

**主要処理フロー**:
- **3行目（initializer）**: ActionViewにLiquidハンドラを登録
- **6-8行目（handler）**: テンプレートソースをコンパイル
- **16-24行目**: テンプレートのレンダリング実行
- **34-37行目**: コントローラからDropオブジェクトを取得

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

```
[キャンペーン送信]
Campaign送信処理
    │
    └─ Liquid::CampaignTemplateService.new(campaign:, contact:).call(message)
           │
           ├─ message_drops
           │      ├─ ContactDrop.new(contact)
           │      ├─ UserDrop.new(campaign.sender)
           │      ├─ InboxDrop.new(campaign.inbox)
           │      └─ AccountDrop.new(campaign.account)
           │
           └─ process_liquid_in_content(drops, message)
                  ├─ gsub(/`(.*?)`/m, ...) [コードブロック保護]
                  ├─ Liquid::Template.parse(message)
                  └─ template.render(drops)

[メッセージ作成]
Message.create
    │
    └─ before_create :process_liquid_in_content
           │
           ├─ liquid_processable_message? [outgoing/template判定]
           │
           ├─ message_drops
           │      ├─ ContactDrop.new(conversation.contact)
           │      ├─ UserDrop.new(sender)
           │      ├─ ConversationDrop.new(conversation)
           │      ├─ InboxDrop.new(inbox)
           │      └─ AccountDrop.new(conversation.account)
           │
           └─ Liquid::Template.parse/render
    │
    └─ before_create :process_liquid_in_template_params
           │
           └─ process_liquid_in_hash(template_params['processed_params'])
                  └─ [再帰的にHash/Array/String内のLiquid変数を処理]
```

### データフロー図

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

メッセージ本文               Liquid::CampaignTemplateService
"Hello {{contact.name}}"  ─────────▶  or Liquidable concern  ─────────▶  "Hello John Doe"
                                            │
                                            ▼
Campaign/Conversation      ┌──────────────────────────────────┐
    ├─ contact  ──────────▶│         Dropオブジェクト          │
    ├─ sender   ──────────▶│  contact: ContactDrop            │
    ├─ inbox    ──────────▶│  agent: UserDrop                 │
    └─ account  ──────────▶│  inbox: InboxDrop                │
                           │  account: AccountDrop            │
                           │  conversation: ConversationDrop  │
                           └──────────────────────────────────┘
                                            │
                                            ▼
                                ┌─────────────────────┐
                                │  Liquid::Template   │
                                │  .parse() → .render()│
                                └─────────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| campaign_template_service.rb | `app/services/liquid/campaign_template_service.rb` | サービス | キャンペーン用Liquidテンプレート処理 |
| liquidable.rb | `app/models/concerns/liquidable.rb` | Concern | メッセージモデル用Liquid処理 |
| base_drop.rb | `app/drops/base_drop.rb` | Drop | 全Dropの基底クラス |
| contact_drop.rb | `app/drops/contact_drop.rb` | Drop | 連絡先情報公開用 |
| user_drop.rb | `app/drops/user_drop.rb` | Drop | ユーザー情報公開用 |
| inbox_drop.rb | `app/drops/inbox_drop.rb` | Drop | インボックス情報公開用 |
| account_drop.rb | `app/drops/account_drop.rb` | Drop | アカウント情報公開用 |
| conversation_drop.rb | `app/drops/conversation_drop.rb` | Drop | 会話情報公開用 |
| message_drop.rb | `app/drops/message_drop.rb` | Drop | メッセージ情報公開用 |
| liquid_handler.rb | `config/initializers/liquid_handler.rb` | 初期化 | Liquidハンドラ登録 |
| liquid.rb | `lib/action_view/template/handlers/liquid.rb` | テンプレートハンドラ | ActionView用Liquidハンドラ |
