# 機能設計書 151-ポータル

## 概要

本ドキュメントは、Chatwootのヘルプセンター機能の中核を担う「ポータル」機能の設計仕様を定義する。ポータルはナレッジベースの基盤となるエンティティであり、記事やカテゴリを管理するための親コンテナとして機能する。

### 本機能の処理概要

**業務上の目的・背景**：
カスタマーサポートにおいて、顧客が自己解決できるナレッジベース（ヘルプセンター）を構築することは、サポートコストの削減とカスタマーエクスペリエンスの向上に直結する。ポータル機能は、企業が独自のヘルプセンターを作成・管理するための基盤を提供し、カスタムドメインの設定、多言語対応、ブランドカスタマイズを可能にする。

**機能の利用シーン**：
- 管理者がカスタマーサポート用のヘルプセンターを新規作成する場面
- 既存ポータルの設定（名前、カラー、カスタムドメイン等）を変更する場面
- 多言語対応のポータルを構築し、各ロケールで記事を提供する場面
- Web Widgetと連携してヘルプ記事をチャット画面内で表示する場面

**主要な処理内容**：
1. ポータルの作成：スラッグ（URL識別子）、名前、カラー設定、カスタムドメインなどの基本情報を登録
2. ポータルの更新：設定情報の変更、ロゴのアップロード、多言語設定の管理
3. ポータルの削除：関連する記事・カテゴリを含めたポータルの完全削除
4. ポータル一覧・詳細表示：アカウント内の全ポータル情報の取得と個別表示
5. Web Widget連携：ポータルをWeb Widgetに紐付けてヘルプ記事を提供

**関連システム・外部連携**：
- ActiveStorage：ロゴ画像のストレージ管理
- Cloudflare（Enterprise版）：カスタムドメインのSSL証明書管理
- Web Widget：ポータルをウィジェット内で表示

**権限による制御**：
- 一覧表示・詳細表示：アカウントに所属する全ユーザーがアクセス可能
- 作成・更新・削除・ロゴ管理：管理者（Administrator）権限が必要

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 91 | ポータル一覧画面 | 主画面 | ヘルプセンターポータルの一覧表示 |
| 92 | ポータル新規作成画面 | 主画面 | 新しいポータルの作成 |
| 98 | ポータル設定画面 | 主画面 | ポータルの詳細設定 |

## 機能種別

CRUD操作 / 設定管理 / ファイルアップロード

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | String | Yes | ポータル名 | presence: true |
| slug | String | Yes | URL識別子 | presence: true, uniqueness: true |
| color | String | No | ブランドカラー | - |
| custom_domain | String | No | カスタムドメイン | uniqueness: true |
| header_text | Text | No | ヘッダーテキスト | - |
| homepage_link | String | No | ホームページリンク | - |
| page_title | String | No | ページタイトル | - |
| config.default_locale | String | No | デフォルトロケール | default: 'en' |
| config.allowed_locales | Array | No | 許可されたロケール | - |
| inbox_id | Integer | No | 連携するWeb Widget | Inbox存在確認 |
| blob_id | String | No | ロゴ画像のBlob ID | ActiveStorage::Blob存在確認 |

### 入力データソース

- 画面入力：管理画面のポータル設定フォーム
- API：`/api/v1/accounts/:account_id/portals` エンドポイント

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | Integer | ポータルID |
| name | String | ポータル名 |
| slug | String | URL識別子 |
| color | String | ブランドカラー |
| custom_domain | String | カスタムドメイン |
| header_text | Text | ヘッダーテキスト |
| homepage_link | String | ホームページリンク |
| page_title | String | ページタイトル |
| archived | Boolean | アーカイブ状態 |
| config | JSON | 設定情報（allowed_locales, default_locale等） |
| ssl_settings | JSON | SSL設定情報 |
| logo | Object | ロゴ画像情報 |
| categories | Array | 関連カテゴリ一覧 |
| articles | Array | 関連記事一覧 |

### 出力先

- JSON API レスポンス
- JBuilder ビューテンプレート

## 処理フロー

### 処理シーケンス

```
1. ポータル作成
   └─ リクエスト受信 → 認可チェック → パラメータ検証 → Portal作成 → ロゴ添付 → レスポンス返却

2. ポータル更新
   └─ リクエスト受信 → Portal取得 → 認可チェック → パラメータ検証 → トランザクション開始 → Portal更新 → ロゴ処理 → コミット → レスポンス返却

3. ポータル削除
   └─ リクエスト受信 → Portal取得 → 認可チェック → 関連データ削除（非同期） → Portal削除 → レスポンス返却
```

### フローチャート

```mermaid
flowchart TD
    A[リクエスト受信] --> B{アクション判定}
    B -->|index| C[アカウントの全ポータル取得]
    B -->|show| D[slug指定でPortal取得]
    B -->|create| E[パラメータ検証]
    B -->|update| F[Portal取得 + パラメータ検証]
    B -->|destroy| G[Portal取得]

    E --> H[Portal作成]
    H --> I{blob_id存在?}
    I -->|Yes| J[ロゴ添付]
    I -->|No| K[レスポンス返却]
    J --> K

    F --> L[トランザクション開始]
    L --> M[Portal更新]
    M --> N{blob_id存在?}
    N -->|Yes| O[ロゴ添付]
    N -->|No| P[コミット]
    O --> P
    P --> K

    G --> Q[destroy!実行]
    Q --> R[HTTP 200 OK]

    C --> K
    D --> K
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-151-01 | スラッグ一意性 | slugはシステム全体で一意でなければならない | Portal作成・更新時 |
| BR-151-02 | カスタムドメイン一意性 | custom_domainはシステム全体で一意（NULLは許容） | カスタムドメイン設定時 |
| BR-151-03 | 許可ロケール検証 | config内のdefault_localeとallowed_localesのみ設定可能 | 設定変更時 |
| BR-151-04 | アーカイブポータル非表示 | archived=trueのポータルは公開APIで非表示 | 公開アクセス時 |
| BR-151-05 | Web Widget連携 | inbox_idが指定された場合、Web Widget型のInboxのchannel_idを保存 | ポータル作成・更新時 |

### 計算ロジック

- デフォルトロケール：`config['default_locale'] || 'en'`
- カスタムドメインのパース：URI.parse後、HTTPスキームの場合はホスト名のみ抽出

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| create | portals | INSERT | 新規ポータルの作成 |
| update | portals | UPDATE | ポータル情報の更新 |
| destroy | portals | DELETE | ポータルの削除 |
| destroy | categories | DELETE | 関連カテゴリの非同期削除 |
| destroy | articles | DELETE | 関連記事の非同期削除 |
| attach_logo | active_storage_attachments | INSERT | ロゴ添付情報 |

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

#### portals

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | name | リクエストパラメータ | 必須 |
| INSERT | slug | リクエストパラメータ | 必須、一意 |
| INSERT | account_id | Current.account.id | 自動設定 |
| INSERT | color | リクエストパラメータ | 任意 |
| INSERT | custom_domain | parsed_custom_domain | URIパース後 |
| INSERT | config | { default_locale, allowed_locales } | JSON型 |
| UPDATE | archived | true/false | archiveアクション時 |
| UPDATE | channel_web_widget_id | Inbox.channel.id | Web Widget連携時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 422 | UnprocessableEntity | slug重複 | 異なるslugを指定 |
| 422 | UnprocessableEntity | custom_domain重複 | 異なるドメインを指定 |
| 404 | NotFound | 指定slugのPortalが存在しない | 正しいslugを指定 |
| 403 | Forbidden | 権限不足 | 管理者権限でアクセス |

### リトライ仕様

特になし

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

- update処理はActiveRecord::Base.transactionでラップ
- 関連データ削除は`dependent: :destroy_async`で非同期実行

## パフォーマンス要件

- ポータル一覧取得：100ms以内
- 記事数が多い場合でもN+1クエリを回避

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

- 管理者権限チェック：PortalPolicyによる認可
- カスタムドメイン：DNSハイジャック防止のためSSL証明書検証（Enterprise版）
- XSS防止：ユーザー入力値のサニタイズ

## 備考

- Enterprise版ではCloudflare統合によるSSL証明書自動発行機能が利用可能
- `Portal.include_mod_with('Concerns::Portal')`により拡張機能を追加可能

---

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

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

### 推奨読解順序

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

まず、ポータルのデータモデルと関連モデルを理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | portal.rb | `app/models/portal.rb` | スキーマ定義、バリデーション、関連モデル |
| 1-2 | category.rb | `app/models/category.rb` | カテゴリとの関連 |
| 1-3 | article.rb | `app/models/article.rb` | 記事との関連 |

**読解のコツ**:
- Schema Informationコメントでテーブル構造を把握
- `belongs_to`/`has_many`で親子関係を理解
- `dependent: :destroy_async`により非同期削除が行われる点に注目

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

APIコントローラーがリクエストの起点となる。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | portals_controller.rb | `app/controllers/api/v1/accounts/portals_controller.rb` | CRUD操作のエントリーポイント |

**主要処理フロー**:
1. **8-10行目**: index - 全ポータル取得
2. **12-15行目**: show - 個別ポータル取得と記事検索
3. **17-22行目**: create - ポータル作成とロゴ添付
4. **24-32行目**: update - トランザクション内での更新
5. **34-37行目**: destroy - ポータル削除
6. **49-60行目**: send_instructions - CNAME設定メール送信

#### Step 3: 認可処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | portal_policy.rb | `app/policies/portal_policy.rb` | アクション毎の権限チェック |

**主要処理フロー**:
- **3-4行目**: index? - アカウントユーザーであれば許可
- **6-8行目**: update? - 管理者のみ許可
- **18-20行目**: create? - 管理者のみ許可

#### Step 4: Enterprise拡張を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | portals_controller.rb | `enterprise/app/controllers/enterprise/api/v1/accounts/portals_controller.rb` | SSL状態確認機能 |

**主要処理フロー**:
- **2-14行目**: ssl_status - Cloudflare連携によるSSL証明書状態確認

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

```
Api::V1::Accounts::PortalsController
    │
    ├─ before_action :fetch_portal
    │      └─ Current.account.portals.find_by(slug:)
    │
    ├─ before_action :check_authorization
    │      └─ PortalPolicy
    │             ├─ index? / show? → account.users.include?(user)
    │             └─ create? / update? / destroy? → account_user.administrator?
    │
    ├─ create
    │      ├─ Portal.build(portal_params.merge(live_chat_widget_params))
    │      ├─ parsed_custom_domain
    │      └─ process_attached_logo
    │             └─ ActiveStorage::Blob.find_signed(blob_id)
    │
    ├─ update
    │      └─ ActiveRecord::Base.transaction
    │             ├─ portal.update!(portal_params)
    │             └─ process_attached_logo
    │
    └─ destroy
           └─ portal.destroy!
                  ├─ categories.destroy_async
                  └─ articles.destroy_async
```

### データフロー図

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

API Request         →   PortalsController        →   JSON Response
(params)                    │
                           ├─ PortalPolicy
                           │     (認可)
                           │
                           ├─ Portal Model
                           │     (バリデーション)
                           │
                           └─ ActiveStorage
                                 (ロゴ管理)
                                       ↓
                              [portals テーブル]
                              [active_storage_* テーブル]
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| portal.rb | `app/models/portal.rb` | モデル | ポータルのデータモデル定義 |
| portals_controller.rb | `app/controllers/api/v1/accounts/portals_controller.rb` | コントローラー | API CRUD操作 |
| portal_policy.rb | `app/policies/portal_policy.rb` | ポリシー | 認可ルール定義 |
| portals_controller.rb | `enterprise/app/controllers/enterprise/api/v1/accounts/portals_controller.rb` | コントローラー | Enterprise SSL機能 |
| portal_instructions_mailer.rb | `app/mailers/portal_instructions_mailer.rb` | メーラー | CNAME設定メール送信 |
| index.json.jbuilder | `app/views/api/v1/accounts/portals/index.json.jbuilder` | ビュー | 一覧表示用JSON生成 |
| show.json.jbuilder | `app/views/api/v1/accounts/portals/show.json.jbuilder` | ビュー | 詳細表示用JSON生成 |
