# 機能設計書 146-Linear統合

## 概要

本ドキュメントは、ChatwootとLinear（課題追跡ツール）を連携する統合機能の設計書である。OAuth認証によりLinearワークスペースと接続し、会話から直接Linearの課題（Issue）を作成・リンク・検索することで、カスタマーサポートとプロダクト開発の連携を効率化する。

### 本機能の処理概要

**業務上の目的・背景**：カスタマーサポートで報告されたバグや機能要望は、プロダクトチームの課題追跡システムに登録する必要がある。Linear統合により、Chatwoot上の会話から直接Linearの課題を作成・リンクできるようになり、サポートチームと開発チームの連携がスムーズになる。また、既存の課題との関連付けも容易になり、重複した課題作成を防止できる。

**機能の利用シーン**：
- 顧客からのバグ報告を会話からLinear課題として作成
- 機能要望をLinearのバックログに直接登録
- 会話を既存のLinear課題にリンク
- 関連するLinear課題を検索して紐付け
- 課題のリンク解除

**主要な処理内容**：
1. Linear OAuth認証によるワークスペース接続
2. チーム・プロジェクト・ユーザー情報の取得
3. 課題（Issue）の作成
4. 会話とLinear課題のリンク・アンリンク
5. 課題の検索
6. 操作のアクティビティメッセージ記録

**関連システム・外部連携**：
- Linear GraphQL API（課題管理、チーム情報取得）

**権限による制御**：
- Linear統合の使用にはlinear_integrationフィーチャーフラグが必要
- LINEAR_CLIENT_ID環境変数の設定が必要

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 69 | Linear連携画面 | 主画面 | Linear統合の設定 |
| 65 | 連携アプリ一覧画面 | 参照画面 | Linear統合の有効化状態表示 |

## 機能種別

データ連携 / OAuth認証 / 課題管理

## 入力仕様

### 入力パラメータ（課題作成）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| team_id | String | Yes | LinearチームID | 有効なチームID |
| title | String | Yes | 課題タイトル | 空でないこと |
| description | String | No | 課題説明 | - |
| assignee_id | String | No | 担当者ID | 有効なユーザーID |
| priority | Integer | No | 優先度（0-4） | 0, 1, 2, 3, 4のいずれか |
| state_id | String | No | ステートID | 有効なステートID |
| project_id | String | No | プロジェクトID | 有効なプロジェクトID |
| label_ids | Array | No | ラベルIDの配列 | 文字列の配列 |
| conversation_id | Integer | Yes | 会話表示ID | 存在する会話 |

### 入力パラメータ（リンク操作）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| issue_id | String | Yes | Linear課題ID | 有効な課題ID |
| title | String | No | リンクタイトル | - |
| link_id | String | 条件付き | リンクID（アンリンク時必須） | 有効なリンクID |
| conversation_id | Integer | Yes | 会話表示ID | 存在する会話 |

### 入力データソース

会話詳細画面のLinear連携UIからの入力

## 出力仕様

### 出力データ（課題作成時）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | String | Linear課題ID |
| title | String | 課題タイトル |
| identifier | String | 課題識別子（例: TEAM-123） |

### 出力データ（リンク時）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | String | 課題ID |
| link | String | ChatwootへのリンクURL |
| link_id | String | Linear添付リンクID |

### 出力先

- Linear（課題作成、リンク作成）
- messagesテーブル（アクティビティメッセージ）

## 処理フロー

### 処理シーケンス（課題作成）

```
1. ユーザーが課題作成を開始
   └─ LinearController#create_issue

2. 会話情報取得
   └─ fetch_conversation

3. Linearクライアント呼び出し
   └─ linear_processor_service.create_issue(params, Current.user)

4. GraphQL Mutation実行
   └─ Linear.new.create_issue

5. アクティビティメッセージ作成
   └─ Linear::ActivityMessageService.perform
   └─ action_type: :issue_created

6. レスポンス返却
   └─ { id, title, identifier }
```

### 処理シーケンス（課題リンク）

```
1. ユーザーがリンク操作を開始
   └─ LinearController#link_issue

2. 会話リンク生成
   └─ conversation_link (Chatwoot URL)

3. Linearクライアント呼び出し
   └─ linear_processor_service.link_issue

4. GraphQL Mutation実行（attachmentLinkURL）
   └─ Linear.new.link_issue

5. アクティビティメッセージ作成
   └─ Linear::ActivityMessageService.perform
   └─ action_type: :issue_linked

6. レスポンス返却
   └─ { id, link, link_id }
```

### フローチャート

```mermaid
flowchart TD
    A[ユーザーが操作開始] --> B{操作種別}
    B -->|teams| C[チーム一覧取得]
    B -->|team_entities| D[チーム詳細取得]
    B -->|create_issue| E[課題作成]
    B -->|link_issue| F[課題リンク]
    B -->|unlink_issue| G[リンク解除]
    B -->|search_issue| H[課題検索]
    B -->|linked_issues| I[リンク済み課題取得]

    C --> J[Linear GraphQL API]
    D --> J
    E --> J
    F --> J
    G --> J
    H --> J
    I --> J

    J --> K{成功?}
    K -->|Yes| L[アクティビティメッセージ作成]
    K -->|No| M[エラーレスポンス]
    L --> N[レスポンス返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-146-01 | フィーチャーフラグ | linear_integrationが有効なアカウントのみ利用可能 | 全操作 |
| BR-146-02 | 環境設定 | LINEAR_CLIENT_IDが設定されている場合のみ利用可能 | 全操作 |
| BR-146-03 | 優先度範囲 | 優先度は0-4の範囲のみ | 課題作成時 |
| BR-146-04 | ラベルID形式 | label_idsは文字列の配列 | 課題作成時 |
| BR-146-05 | ユーザー帰属 | 課題作成時にユーザー名とアバターを付与 | 課題作成時 |
| BR-146-06 | アクティビティ記録 | 課題作成・リンク・アンリンク時にアクティビティメッセージ作成 | 該当操作時 |
| BR-146-07 | トークン失効削除 | Hook削除時にLinearトークンを失効させる | Hook削除時 |

### 計算ロジック

特になし

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 認証 | integrations_hooks | INSERT | Linear Hook作成 |
| 削除 | integrations_hooks | DELETE | Linear Hook削除 |
| アクティビティ | messages | INSERT | アクティビティメッセージ作成 |

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

#### integrations_hooks

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | * | app_id = 'linear' AND account_id = ? | Hook取得 |
| DELETE | - | id = ? | Hook削除 |

#### messages

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | message_type | 'activity' | アクティビティメッセージ |
| INSERT | content | "linked Linear issue #{issue_id}" 等 | 操作内容 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 422 | Unprocessable Entity | チームID未指定 | チームIDを指定する |
| 422 | Unprocessable Entity | タイトル未指定 | タイトルを入力する |
| 422 | Unprocessable Entity | 優先度が範囲外 | 0-4の値を指定する |
| 422 | Unprocessable Entity | label_idsが不正 | 文字列配列を指定する |
| 422 | Unprocessable Entity | Linear API エラー | エラーメッセージを確認 |
| 422 | Unprocessable Entity | 検索文字列未指定 | qパラメータを指定する |

### リトライ仕様

Linear API呼び出し自体にはリトライ仕様は存在しない

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

- 各操作は単一リクエストで処理
- アクティビティメッセージ作成は操作成功後に実行

## パフォーマンス要件

- API呼び出し: 5秒以内

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

- access_tokenは暗号化して保存
- Hook削除時にLinearトークンを失効（revoke_token）
- OAuth認証時のstateパラメータでCSRF防止

## 備考

- Linear APIはGraphQLベース
- 課題作成時にcreateAsUserでユーザー名を付与
- 会話リンクはLinearのattachmentLinkURL機能を使用

---

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

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

### 推奨読解順序

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

Linear統合の設定とAPIクライアントを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | apps.yml | `config/integration/apps.yml` | Linearアプリ設定（59-65行目） |
| 1-2 | linear.rb | `lib/linear.rb` | Linear APIクライアント |

**読解のコツ**:
- **apps.yml 59-65行目**: Linearのhook_type(account)、action(OAuth URL)を確認
- **linear.rb**: GraphQLクエリとミューテーションの構造を理解

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

Linear統合コントローラーを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | linear_controller.rb | `app/controllers/api/v1/accounts/integrations/linear_controller.rb` | 各種API操作 |

**主要処理フロー**:
1. **5-9行目**: `destroy`でトークン失効とHook削除
2. **11-18行目**: `teams`でチーム一覧取得
3. **20-28行目**: `team_entities`でチームのエンティティ取得
4. **30-43行目**: `create_issue`で課題作成とアクティビティ記録
5. **45-59行目**: `link_issue`でリンク作成
6. **62-78行目**: `unlink_issue`でリンク解除
7. **80-88行目**: `linked_issues`でリンク済み課題取得
8. **90-100行目**: `search_issue`で課題検索

#### Step 3: サービス処理を理解する

ProcessorServiceの処理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | processor_service.rb | `lib/integrations/linear/processor_service.rb` | Linear API呼び出し |

**主要処理フロー**:
- **4-9行目**: `teams`でチーム一覧取得
- **11-23行目**: `team_entities`でユーザー、プロジェクト、ステート、ラベル取得
- **25-34行目**: `create_issue`で課題作成
- **36-47行目**: `link_issue`でリンク作成
- **49-56行目**: `unlink_issue`でリンク解除
- **58-64行目**: `search_issue`で課題検索
- **66-71行目**: `linked_issues`でリンク済み課題取得

#### Step 4: アクティビティメッセージを理解する

アクティビティメッセージ作成を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | activity_message_service.rb | `app/services/linear/activity_message_service.rb` | アクティビティ作成 |

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

```
LinearController
    │
    ├─ #teams
    │      └─ linear_processor_service.teams
    │             └─ Linear.new.teams
    │                    └─ GraphQL: TEAMS_QUERY
    │
    ├─ #team_entities
    │      └─ linear_processor_service.team_entities
    │             └─ Linear.new.team_entities
    │
    ├─ #create_issue
    │      ├─ fetch_conversation
    │      ├─ linear_processor_service.create_issue
    │      │      └─ Linear.new.create_issue
    │      │             └─ GraphQL: issue_create
    │      └─ Linear::ActivityMessageService.perform
    │             └─ action_type: :issue_created
    │
    ├─ #link_issue
    │      ├─ fetch_conversation
    │      ├─ linear_processor_service.link_issue
    │      │      └─ Linear.new.link_issue
    │      │             └─ GraphQL: attachmentLinkURL
    │      └─ Linear::ActivityMessageService.perform
    │             └─ action_type: :issue_linked
    │
    ├─ #unlink_issue
    │      ├─ fetch_conversation
    │      ├─ linear_processor_service.unlink_issue
    │      │      └─ Linear.new.unlink_issue
    │      │             └─ GraphQL: attachmentDelete
    │      └─ Linear::ActivityMessageService.perform
    │             └─ action_type: :issue_unlinked
    │
    └─ #destroy
           ├─ revoke_linear_token
           │      └─ Linear.new.revoke_token
           └─ hook.destroy!
```

### データフロー図

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

OAuth認証コード        ───▶ HookBuilder              ───▶ integrations_hooks
                               │                              (access_token)
                               ▼
チーム選択             ───▶ ProcessorService          ───▶ チーム情報
                               │
                               ▼
課題作成リクエスト     ───▶ Linear GraphQL API       ───▶ Linear課題
                               │
                               ▼
                       ActivityMessageService        ───▶ messages
                       (アクティビティ記録)                 (activity)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| linear_controller.rb | `app/controllers/api/v1/accounts/integrations/linear_controller.rb` | コントローラー | Linear統合API |
| processor_service.rb | `lib/integrations/linear/processor_service.rb` | サービス | Linear API呼び出し |
| linear.rb | `lib/linear.rb` | クライアント | Linear GraphQLクライアント |
| queries.rb | `lib/linear/queries.rb` | GraphQL | クエリ定義 |
| mutations.rb | `lib/linear/mutations.rb` | GraphQL | ミューテーション定義 |
| activity_message_service.rb | `app/services/linear/activity_message_service.rb` | サービス | アクティビティメッセージ |
| integration_helper.rb | `app/helpers/linear/integration_helper.rb` | ヘルパー | トークン生成 |
| callbacks_controller.rb | `app/controllers/linear/callbacks_controller.rb` | コントローラー | OAuthコールバック |
| apps.yml | `config/integration/apps.yml` | 設定 | Linearアプリ設定 |
